Merge branch 'develop' of https://code.gab.com/gab/social/gab-social into feature/frontend_refactor
This commit is contained in:
commit
0a3c6cea89
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,11 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
Changes to Gab Social will be documented in this file.
|
|
||||||
|
|
||||||
## [1.0.0] - 2019-07-04
|
|
||||||
|
|
||||||
- Mastodon renamed to Gab Social
|
|
||||||
- Mastodon UI replaced with new Gab design
|
|
||||||
- Mastodon streaming server re-written for Node 10
|
|
||||||
- Qualified on Postgres 11
|
|
||||||
- Documentation moved to separate repo
|
|
12
README.md
12
README.md
|
@ -8,21 +8,21 @@ Our goal is to establish the foundation of a federated network of social network
|
||||||
|
|
||||||
## Project goals
|
## Project goals
|
||||||
|
|
||||||
We have diverged from Gab Social in several ways in pursuit of our own goals.
|
We have diverged from Mastodon in several ways in pursuit of our own goals.
|
||||||
|
|
||||||
1. Node.js has been updated to 10.15.3LTS for hosting the Streaming API in compliance with the Gab Platform.
|
1. Node.js has been updated to 10.15.3LTS for hosting the Streaming API in compliance with the Gab Platform.
|
||||||
1. Statuses were renamed from 'toots' to 'gabs'
|
1. Statuses were renamed from 'toots' to 'gabs'
|
||||||
1. The maximum length of a status was increased to 3,000 characters
|
1. The maximum length of a status was increased to 3,000 characters
|
||||||
1. Advanced media (MP4, WebM, etc.) was limited to PRO subscribers
|
|
||||||
1. The creation of custom emoji was limited to PRO subscribers
|
|
||||||
1. The browser client user experience has been significantly altered to match what users of Gab will expect
|
1. The browser client user experience has been significantly altered to match what users of Gab will expect
|
||||||
1. Features were added to integrate the system with the Gab platform (accessing trends from Dissenter, for example)
|
1. Features were added to integrate the system with the Gab platform (accessing trends from Dissenter, for example)
|
||||||
|
1. Groups and group moderation
|
||||||
|
1. Quote posting
|
||||||
|
|
||||||
## BTCPay
|
## BTCPay
|
||||||
In order to make BTC flow work, 3 enviornment variables need to be set:
|
In order to make BTC flow work, 3 environment variables need to be set:
|
||||||
|
|
||||||
- `BTCPAY_LEGACY_TOKEN`: So called Legacy Tokens can be found in https://btcpay.xxx.com/stores/yyy/Tokens
|
- `BTCPAY_LEGACY_TOKEN`: So called Legacy Tokens can be found in https://btcpay.[yourdomain].com/stores/[yourstore]/Tokens
|
||||||
- `BTCPAY_PUB_KEY`: Public key that is used when creating an access token or pairing https://btcpay.xxx.com/stores/yyy/Tokens/Create
|
- `BTCPAY_PUB_KEY`: Public key that is used when creating an access token or pairing https://btcpay.[yourdomain].com/stores/[yourstore]/Tokens/Create
|
||||||
- `BTCPAY_MERCHANT_TOKEN`: Token created for facade *merchant*
|
- `BTCPAY_MERCHANT_TOKEN`: Token created for facade *merchant*
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class AccountsController < BaseController
|
class AccountsController < BaseController
|
||||||
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject, :verify, :unverify, :add_donor_badge, :remove_donor_badge, :add_investor_badge, :remove_investor_badge, :edit_pro, :save_pro]
|
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject, :verify, :unverify, :add_donor_badge, :remove_donor_badge, :add_investor_badge, :remove_investor_badge, :edit_pro, :save_pro, :edit, :update]
|
||||||
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
|
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
|
||||||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||||
|
|
||||||
|
@ -173,6 +173,22 @@ module Admin
|
||||||
redirect_to edit_pro_admin_account_path(@account.id)
|
redirect_to edit_pro_admin_account_path(@account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
redirect_to admin_account_path(@account.id) unless @account.local?
|
||||||
|
@user = @account.user
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
redirect_to admin_account_path(@account.id) unless @account.local?
|
||||||
|
@user = @account.user
|
||||||
|
if @user.update(credentials_params)
|
||||||
|
redirect_to admin_account_path(@account.id), notice: I18n.t('generic.changes_saved_msg')
|
||||||
|
else
|
||||||
|
render action: :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
@ -211,5 +227,14 @@ module Admin
|
||||||
def pro_params
|
def pro_params
|
||||||
params.require(:account).permit(:is_pro, :pro_expires_at)
|
params.require(:account).permit(:is_pro, :pro_expires_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def credentials_params
|
||||||
|
new_params = params.require(:user).permit(:email, :password, :password_confirmation)
|
||||||
|
if new_params[:password].blank? && new_params[:password_confirmation].blank?
|
||||||
|
new_params.delete(:password)
|
||||||
|
new_params.delete(:password_confirmation)
|
||||||
|
end
|
||||||
|
new_params
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
# 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
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,7 +11,12 @@ class Api::V1::AccountByUsernameController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find_local!(params[:username])
|
username, domain = params[:username].split("@")
|
||||||
|
if domain
|
||||||
|
@account = Account.find_remote!(username, domain)
|
||||||
|
else
|
||||||
|
@account = Account.find_local!(username)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
|
|
|
@ -20,13 +20,19 @@ class Api::V1::Groups::AccountsController < Api::BaseController
|
||||||
authorize @group, :join?
|
authorize @group, :join?
|
||||||
|
|
||||||
@group.accounts << current_account
|
@group.accounts << current_account
|
||||||
|
|
||||||
|
if current_user.allows_group_in_home_feed?
|
||||||
|
current_user.force_regeneration!
|
||||||
|
end
|
||||||
|
|
||||||
render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships
|
render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize @group, :update_account?
|
authorize @group, :update_account?
|
||||||
|
|
||||||
GroupAccount.where(group: @group, account_id: current_account.id).update(group_account_params)
|
@account = @group.accounts.find(params[:account_id])
|
||||||
|
GroupAccount.where(group: @group, account: @account).update(group_account_params)
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -34,6 +40,11 @@ class Api::V1::Groups::AccountsController < Api::BaseController
|
||||||
authorize @group, :leave?
|
authorize @group, :leave?
|
||||||
|
|
||||||
GroupAccount.where(group: @group, account_id: current_account.id).destroy_all
|
GroupAccount.where(group: @group, account_id: current_account.id).destroy_all
|
||||||
|
|
||||||
|
if current_user.allows_group_in_home_feed?
|
||||||
|
current_user.force_regeneration!
|
||||||
|
end
|
||||||
|
|
||||||
render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships
|
render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Api::V1::GroupsController < Api::BaseController
|
||||||
def index
|
def index
|
||||||
case current_tab
|
case current_tab
|
||||||
when 'featured'
|
when 'featured'
|
||||||
@groups = Group.where(is_featured: true).limit(25).all
|
@groups = Group.where(is_featured: true).limit(50).all
|
||||||
when 'member'
|
when 'member'
|
||||||
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).order('group_accounts.unread_count DESC, group_accounts.id DESC').all
|
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).order('group_accounts.unread_count DESC, group_accounts.id DESC').all
|
||||||
when 'admin'
|
when 'admin'
|
||||||
|
@ -33,6 +33,8 @@ class Api::V1::GroupsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize :group, :create?
|
||||||
|
|
||||||
@group = Group.create!(group_params.merge(account: current_account))
|
@group = Group.create!(group_params.merge(account: current_account))
|
||||||
render json: @group, serializer: REST::GroupSerializer
|
render json: @group, serializer: REST::GroupSerializer
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::MediaController < Api::BaseController
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@media = current_account.media_attachments.create!(media_params)
|
@media = current_account.media_attachments.create!(account: current_account, file: media_params[:file], description: media_params[:description], focus: media_params[:focus])
|
||||||
render json: @media, serializer: REST::MediaAttachmentSerializer
|
render json: @media, serializer: REST::MediaAttachmentSerializer
|
||||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
||||||
render json: file_type_error, status: 422
|
render json: file_type_error, status: 422
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::NotificationsController < Api::BaseController
|
class Api::V1::NotificationsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss]
|
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss, :mark_read]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss]
|
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss, :mark_read]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
after_action :insert_pagination_headers, only: :index
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
@ -30,6 +30,11 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mark_read
|
||||||
|
current_account.notifications.find(params[:id]).mark_read!
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_notifications
|
def load_notifications
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::SearchController < Api::BaseController
|
class Api::V1::SearchController < Api::BaseController
|
||||||
RESULTS_LIMIT = 20
|
RESULTS_LIMIT = 100
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
class Api::V1::StatusesController < Api::BaseController
|
class Api::V1::StatusesController < Api::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||||
before_action :require_user!, except: [:show, :context, :card]
|
before_action :require_user!, except: [:show, :context, :card]
|
||||||
before_action :set_status, only: [:show, :context, :card]
|
before_action :set_status, only: [:show, :context, :card, :update, :revisions]
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
|
@ -33,14 +33,10 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def card
|
def revisions
|
||||||
@card = @status.preview_cards.first
|
@revisions = @status.revisions
|
||||||
|
|
||||||
if @card.nil?
|
render json: @revisions, each_serializer: REST::StatusRevisionSerializer
|
||||||
render_empty
|
|
||||||
else
|
|
||||||
render json: @card, serializer: REST::PreviewCardSerializer
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -55,11 +51,27 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
application: doorkeeper_token.application,
|
application: doorkeeper_token.application,
|
||||||
poll: status_params[:poll],
|
poll: status_params[:poll],
|
||||||
idempotency: request.headers['Idempotency-Key'],
|
idempotency: request.headers['Idempotency-Key'],
|
||||||
group_id: status_params[:group_id])
|
group_id: status_params[:group_id],
|
||||||
|
quote_of_id: status_params[:quote_of_id])
|
||||||
|
|
||||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @status, :update?
|
||||||
|
|
||||||
|
@status = EditStatusService.new.call(@status,
|
||||||
|
text: status_params[:status],
|
||||||
|
media_ids: status_params[:media_ids],
|
||||||
|
sensitive: status_params[:sensitive],
|
||||||
|
spoiler_text: status_params[:spoiler_text],
|
||||||
|
visibility: status_params[:visibility],
|
||||||
|
application: doorkeeper_token.application,
|
||||||
|
idempotency: request.headers['Idempotency-Key'])
|
||||||
|
|
||||||
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@status = Status.where(account_id: current_user.account).find(params[:id])
|
@status = Status.where(account_id: current_user.account).find(params[:id])
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
@ -82,6 +94,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
params.permit(
|
params.permit(
|
||||||
:status,
|
:status,
|
||||||
:in_reply_to_id,
|
:in_reply_to_id,
|
||||||
|
:quote_of_id,
|
||||||
:sensitive,
|
:sensitive,
|
||||||
:spoiler_text,
|
:spoiler_text,
|
||||||
:visibility,
|
:visibility,
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController
|
||||||
statuses = group_timeline_statuses.without_replies.paginate_by_id(
|
statuses = group_timeline_statuses.without_replies.paginate_by_id(
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) }
|
||||||
|
|
||||||
if truthy_param?(:only_media)
|
if truthy_param?(:only_media)
|
||||||
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
|
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
|
||||||
|
|
|
@ -4,6 +4,7 @@ class HomeController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_referrer_policy_header
|
before_action :set_referrer_policy_header
|
||||||
before_action :set_initial_state_json
|
before_action :set_initial_state_json
|
||||||
|
before_action :set_data_for_meta
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
@body_classes = 'app-body'
|
||||||
|
@ -11,17 +12,40 @@ class HomeController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_data_for_meta
|
||||||
|
return if find_route_matches
|
||||||
|
|
||||||
|
if params[:username].present?
|
||||||
|
@account = Account.find_local(params[:username])
|
||||||
|
elsif params[:account_username].present?
|
||||||
|
@account = Account.find_local(params[:account_username])
|
||||||
|
|
||||||
|
if params[:id].present? && !@account.nil?
|
||||||
|
@status = @account.statuses.find(params[:id])
|
||||||
|
@stream_entry = @status.stream_entry
|
||||||
|
@type = @stream_entry.activity_type.downcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if request.path.starts_with?('/tags') && params[:tag].present?
|
||||||
|
@tag = Tag.find_normalized(params[:tag])
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
def authenticate_user!
|
def authenticate_user!
|
||||||
return if user_signed_in?
|
return if user_signed_in?
|
||||||
|
|
||||||
# if no current user, dont allow to navigate to these paths
|
# if no current user, dont allow to navigate to these paths
|
||||||
matches = request.path.match(/\A\/(home|groups|tags|lists|notifications|explore|follow_requests|blocks|domain_blocks|mutes)/)
|
if find_route_matches
|
||||||
|
|
||||||
if matches
|
|
||||||
redirect_to(homepage_path)
|
redirect_to(homepage_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_route_matches
|
||||||
|
request.path.match(/\A\/(home|groups|lists|notifications|explore|follow_requests|blocks|domain_blocks|mutes)/)
|
||||||
|
end
|
||||||
|
|
||||||
def set_initial_state_json
|
def set_initial_state_json
|
||||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
|
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
|
||||||
@initial_state_json = serializable_resource.to_json
|
@initial_state_json = serializable_resource.to_json
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
class Settings::ExpensesController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
@ammount = Redis.current.get("monthly_funding_ammount") || 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
Redis.current.set("monthly_funding_ammount", params[:ammount])
|
||||||
|
redirect_to settings_expenses_path
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -25,7 +25,7 @@ class Settings::NotificationsController < Settings::BaseController
|
||||||
|
|
||||||
def user_settings_params
|
def user_settings_params
|
||||||
params.require(:user).permit(
|
params.require(:user).permit(
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
|
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account emails_from_gabcom),
|
||||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Settings::PreferencesController < Settings::BaseController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
user_settings.update(user_settings_params.to_h)
|
user_settings.update(user_settings_params.to_h)
|
||||||
|
current_user.force_regeneration!
|
||||||
|
|
||||||
if current_user.update(user_params)
|
if current_user.update(user_params)
|
||||||
I18n.locale = current_user.locale
|
I18n.locale = current_user.locale
|
||||||
|
@ -51,7 +52,7 @@ class Settings::PreferencesController < Settings::BaseController
|
||||||
:setting_show_application,
|
:setting_show_application,
|
||||||
:setting_advanced_layout,
|
:setting_advanced_layout,
|
||||||
:setting_group_in_home_feed,
|
:setting_group_in_home_feed,
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
|
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account emails_from_gabcom),
|
||||||
interactions: %i(must_be_follower must_be_following)
|
interactions: %i(must_be_follower must_be_following)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
class Settings::PromotionsController < Admin::BaseController
|
||||||
|
before_action :set_promotion, except: [:index, :new, :create]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@promotions = Promotion.all
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@promotion = Promotion.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@promotion = Promotion.new(resource_params)
|
||||||
|
|
||||||
|
if @promotion.save
|
||||||
|
log_action :create, @promotion
|
||||||
|
redirect_to settings_promotions_path, notice: I18n.t('promotions.created_msg')
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @promotion.update(resource_params)
|
||||||
|
log_action :update, @promotion
|
||||||
|
flash[:notice] = I18n.t('promotions.updated_msg')
|
||||||
|
else
|
||||||
|
flash[:alert] = I18n.t('promotions.update_failed_msg')
|
||||||
|
end
|
||||||
|
redirect_to settings_promotions_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@promotion.destroy!
|
||||||
|
log_action :destroy, @promotion
|
||||||
|
flash[:notice] = I18n.t('promotions.destroyed_msg')
|
||||||
|
redirect_to settings_promotions_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_promotion
|
||||||
|
@promotion = Promotion.find(params[:id])
|
||||||
|
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
|
||||||
|
end
|
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::ScheduledStatusesController < Settings::BaseController
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_account
|
||||||
|
before_action :set_scheduled_statuses, only: :index
|
||||||
|
before_action :set_scheduled_status, only: :destroy
|
||||||
|
|
||||||
|
def index
|
||||||
|
@scheduled_statuses
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@scheduled_status.destroy!
|
||||||
|
redirect_to settings_scheduled_statuses_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = current_user.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_scheduled_statuses
|
||||||
|
@scheduled_statuses = @account.scheduled_statuses
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_scheduled_status
|
||||||
|
@scheduled_status = @account.scheduled_statuses.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { CancelToken, isCancel } from 'axios';
|
import { CancelToken, isCancel } from 'axios';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
import { search as emojiSearch } from '../components/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from '../components/emoji/emoji_mart_search_light';
|
||||||
import { tagHistory } from '../settings';
|
import { tagHistory } from '../settings';
|
||||||
import { useEmoji } from './emojis';
|
import { useEmoji } from './emojis';
|
||||||
|
@ -20,6 +21,7 @@ export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
||||||
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
||||||
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
||||||
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
||||||
|
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
||||||
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
||||||
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
||||||
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
||||||
|
@ -60,6 +62,8 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
|
||||||
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
|
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
|
||||||
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
||||||
|
|
||||||
|
export const COMPOSE_SCHEDULED_AT_CHANGE = 'COMPOSE_SCHEDULED_AT_CHANGE';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
@ -91,6 +95,17 @@ export function replyCompose(status, routerHistory) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function quoteCompose(status, routerHistory) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_QUOTE,
|
||||||
|
status: status,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('COMPOSE'));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function cancelReplyCompose() {
|
export function cancelReplyCompose() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_REPLY_CANCEL,
|
type: COMPOSE_REPLY_CANCEL,
|
||||||
|
@ -125,6 +140,45 @@ export function directCompose(account, routerHistory) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function handleComposeSubmit(dispatch, getState, response, status) {
|
||||||
|
if (!dispatch || !getState) return;
|
||||||
|
|
||||||
|
const isScheduledStatus = response.data['scheduled_at'] !== undefined;
|
||||||
|
if (isScheduledStatus) {
|
||||||
|
dispatch(showAlertForError({
|
||||||
|
response: {
|
||||||
|
data: {},
|
||||||
|
status: 200,
|
||||||
|
statusText: 'Successfully scheduled status',
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
dispatch(submitComposeSuccess({ ...response.data }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||||
|
dispatch(submitComposeSuccess({ ...response.data }));
|
||||||
|
|
||||||
|
// To make the app more responsive, immediately push the status into the columns
|
||||||
|
const insertIfOnline = timelineId => {
|
||||||
|
const timeline = getState().getIn(['timelines', timelineId]);
|
||||||
|
|
||||||
|
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
|
||||||
|
let dequeueArgs = {};
|
||||||
|
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']);
|
||||||
|
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
|
||||||
|
dispatch(updateTimeline(timelineId, { ...response.data }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response.data.visibility !== 'direct') {
|
||||||
|
insertIfOnline('home');
|
||||||
|
} else if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||||
|
insertIfOnline('community');
|
||||||
|
insertIfOnline('public');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function submitCompose(routerHistory, group) {
|
export function submitCompose(routerHistory, group) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
@ -139,9 +193,20 @@ export function submitCompose(routerHistory, group) {
|
||||||
dispatch(submitComposeRequest());
|
dispatch(submitComposeRequest());
|
||||||
dispatch(closeModal());
|
dispatch(closeModal());
|
||||||
|
|
||||||
api(getState).post('/api/v1/statuses', {
|
const id = getState().getIn(['compose', 'id']);
|
||||||
|
const endpoint = id === null
|
||||||
|
? '/api/v1/statuses'
|
||||||
|
: `/api/v1/statuses/${id}`;
|
||||||
|
const method = id === null ? 'post' : 'put';
|
||||||
|
|
||||||
|
let scheduled_at = getState().getIn(['compose', 'scheduled_at'], null);
|
||||||
|
if (scheduled_at !== null) scheduled_at = moment.utc(scheduled_at).toDate();
|
||||||
|
|
||||||
|
api(getState)[method](endpoint, {
|
||||||
status,
|
status,
|
||||||
|
scheduled_at,
|
||||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||||
|
quote_of_id: getState().getIn(['compose', 'quote_of_id'], null),
|
||||||
media_ids: media.map(item => item.get('id')),
|
media_ids: media.map(item => item.get('id')),
|
||||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||||
|
@ -156,33 +221,7 @@ export function submitCompose(routerHistory, group) {
|
||||||
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
|
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
|
||||||
routerHistory.push('/messages');
|
routerHistory.push('/messages');
|
||||||
}
|
}
|
||||||
|
handleComposeSubmit(dispatch, getState, response, status);
|
||||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
|
||||||
dispatch(submitComposeSuccess({ ...response.data }));
|
|
||||||
|
|
||||||
// To make the app more responsive, immediately push the status
|
|
||||||
// into the columns
|
|
||||||
|
|
||||||
const insertIfOnline = timelineId => {
|
|
||||||
const timeline = getState().getIn(['timelines', timelineId]);
|
|
||||||
|
|
||||||
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
|
|
||||||
let dequeueArgs = {};
|
|
||||||
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']),
|
|
||||||
|
|
||||||
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
|
|
||||||
dispatch(updateTimeline(timelineId, { ...response.data }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (response.data.visibility !== 'direct') {
|
|
||||||
insertIfOnline('home');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
|
||||||
insertIfOnline('community');
|
|
||||||
insertIfOnline('public');
|
|
||||||
}
|
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(submitComposeFail(error));
|
dispatch(submitComposeFail(error));
|
||||||
});
|
});
|
||||||
|
@ -561,3 +600,10 @@ export function changePollSettings(expiresIn, isMultiple) {
|
||||||
isMultiple,
|
isMultiple,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function changeScheduledAt(date) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SCHEDULED_AT_CHANGE,
|
||||||
|
date,
|
||||||
|
};
|
||||||
|
};
|
|
@ -51,6 +51,10 @@ export const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST';
|
||||||
export const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
|
export const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
|
||||||
export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL';
|
export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_UPDATE_ROLE_REQUEST = 'GROUP_UPDATE_ROLE_REQUEST';
|
||||||
|
export const GROUP_UPDATE_ROLE_SUCCESS = 'GROUP_UPDATE_ROLE_SUCCESS';
|
||||||
|
export const GROUP_UPDATE_ROLE_FAIL = 'GROUP_UPDATE_ROLE_FAIL';
|
||||||
|
|
||||||
export const fetchGroup = id => (dispatch, getState) => {
|
export const fetchGroup = id => (dispatch, getState) => {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
|
@ -521,4 +525,43 @@ export function groupRemoveStatusFail(groupId, id, error) {
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function updateRole(groupId, id, role) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
dispatch(updateRoleRequest(groupId, id));
|
||||||
|
|
||||||
|
api(getState).patch(`/api/v1/groups/${groupId}/accounts?account_id=${id}`, { role }).then(response => {
|
||||||
|
dispatch(updateRoleSuccess(groupId, id));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(updateRoleFail(groupId, id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function updateRoleRequest(groupId, id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function updateRoleSuccess(groupId, id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function updateRoleFail(groupId, id, error) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
};
|
};
|
|
@ -71,6 +71,10 @@ export function importFetchedStatuses(statuses) {
|
||||||
processStatus(status.reblog);
|
processStatus(status.reblog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.quote && status.quote.id) {
|
||||||
|
processStatus(status.quote);
|
||||||
|
}
|
||||||
|
|
||||||
if (status.poll && status.poll.id) {
|
if (status.poll && status.poll.id) {
|
||||||
pushUnique(polls, normalizePoll(status.poll));
|
pushUnique(polls, normalizePoll(status.poll));
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,13 +43,17 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
normalStatus.reblog = status.reblog.id;
|
normalStatus.reblog = status.reblog.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.quote && status.quote.id) {
|
||||||
|
normalStatus.quote = status.quote.id;
|
||||||
|
}
|
||||||
|
|
||||||
if (status.poll && status.poll.id) {
|
if (status.poll && status.poll.id) {
|
||||||
normalStatus.poll = status.poll.id;
|
normalStatus.poll = status.poll.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only calculate these values when status first encountered
|
// Only calculate these values when status first encountered
|
||||||
// Otherwise keep the ones already in the reducer
|
// Otherwise keep the ones already in the reducer
|
||||||
if (normalOldStatus) {
|
if (normalOldStatus && normalOldStatus.get('content') === normalStatus.content && normalOldStatus.get('spoiler_text') === normalStatus.spoiler_text) {
|
||||||
normalStatus.search_index = normalOldStatus.get('search_index');
|
normalStatus.search_index = normalOldStatus.get('search_index');
|
||||||
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
||||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { unescapeHTML } from '../utils/html';
|
||||||
import { getFilters, regexFromFilters } from '../selectors';
|
import { getFilters, regexFromFilters } from '../selectors';
|
||||||
import { me } from 'gabsocial/initial_state';
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_INITIALIZE = 'NOTIFICATIONS_INITIALIZE';
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||||
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
|
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
|
||||||
|
@ -27,6 +28,7 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
||||||
|
|
||||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||||
|
export const NOTIFICATIONS_MARK_READ = 'NOTIFICATIONS_MARK_READ';
|
||||||
|
|
||||||
export const MAX_QUEUED_NOTIFICATIONS = 40;
|
export const MAX_QUEUED_NOTIFICATIONS = 40;
|
||||||
|
|
||||||
|
@ -43,6 +45,12 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function initializeNotifications() {
|
||||||
|
return {
|
||||||
|
type: NOTIFICATIONS_INITIALIZE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function updateNotifications(notification, intlMessages, intlLocale) {
|
export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
||||||
|
@ -134,6 +142,7 @@ export function dequeueNotifications() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_DEQUEUE,
|
type: NOTIFICATIONS_DEQUEUE,
|
||||||
});
|
});
|
||||||
|
dispatch(markReadNotifications());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -225,10 +234,13 @@ export function clearNotifications() {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function scrollTopNotifications(top) {
|
export function scrollTopNotifications(top) {
|
||||||
return {
|
return (dispatch, getState) => {
|
||||||
type: NOTIFICATIONS_SCROLL_TOP,
|
dispatch({
|
||||||
top,
|
type: NOTIFICATIONS_SCROLL_TOP,
|
||||||
};
|
top,
|
||||||
|
});
|
||||||
|
dispatch(markReadNotifications());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setFilter (filterType) {
|
export function setFilter (filterType) {
|
||||||
|
@ -242,3 +254,20 @@ export function setFilter (filterType) {
|
||||||
dispatch(saveSettings());
|
dispatch(saveSettings());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function markReadNotifications() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
const top_notification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']));
|
||||||
|
const last_read = getState().getIn(['notifications', 'lastRead']);
|
||||||
|
|
||||||
|
if (top_notification && top_notification > last_read) {
|
||||||
|
api(getState).post('/api/v1/notifications/mark_read', {id: top_notification}).then(response => {
|
||||||
|
dispatch({
|
||||||
|
type: NOTIFICATIONS_MARK_READ,
|
||||||
|
notification: top_notification,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -37,7 +37,6 @@ export function submitSearch() {
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: true,
|
resolve: true,
|
||||||
limit: 5,
|
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
if (response.data.accounts) {
|
if (response.data.accounts) {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const SIDEBAR_OPEN = 'SIDEBAR_OPEN';
|
||||||
|
export const SIDEBAR_CLOSE = 'SIDEBAR_CLOSE';
|
||||||
|
|
||||||
|
export function openSidebar() {
|
||||||
|
return {
|
||||||
|
type: SIDEBAR_OPEN,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function closeSidebar() {
|
||||||
|
return {
|
||||||
|
type: SIDEBAR_CLOSE,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const STATUS_REVISION_LIST_LOAD = 'STATUS_REVISION_LIST';
|
||||||
|
export const STATUS_REVISION_LIST_LOAD_SUCCESS = 'STATUS_REVISION_LIST_SUCCESS';
|
||||||
|
export const STATUS_REVISION_LIST_LOAD_FAIL = 'STATUS_REVISION_LIST_FAIL';
|
||||||
|
|
||||||
|
const loadSuccess = data => ({ type: STATUS_REVISION_LIST_LOAD_SUCCESS, payload: data });
|
||||||
|
const loadFail = e => ({ type: STATUS_REVISION_LIST_LOAD_FAIL, payload: e });
|
||||||
|
|
||||||
|
export function load(statusId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
api(getState).get(`/api/v1/statuses/${statusId}/revisions`)
|
||||||
|
.then(res => dispatch(loadSuccess(res.data)))
|
||||||
|
.catch(e => dispatch(loadFail(e)));
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import openDB from '../storage/db';
|
||||||
import { evictStatus } from '../storage/modifier';
|
import { evictStatus } from '../storage/modifier';
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
|
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
|
||||||
import { ensureComposeIsVisible } from './compose';
|
import { openModal } from './modal';
|
||||||
import { me } from 'gabsocial/initial_state';
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
|
||||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||||
|
@ -29,7 +29,7 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
|
||||||
export const STATUS_REVEAL = 'STATUS_REVEAL';
|
export const STATUS_REVEAL = 'STATUS_REVEAL';
|
||||||
export const STATUS_HIDE = 'STATUS_HIDE';
|
export const STATUS_HIDE = 'STATUS_HIDE';
|
||||||
|
|
||||||
export const REDRAFT = 'REDRAFT';
|
export const STATUS_EDIT = 'STATUS_EDIT';
|
||||||
|
|
||||||
export function fetchStatusRequest(id, skipLoading) {
|
export function fetchStatusRequest(id, skipLoading) {
|
||||||
return {
|
return {
|
||||||
|
@ -132,15 +132,18 @@ export function fetchStatusFail(id, error, skipLoading) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function redraft(status, raw_text) {
|
export function editStatus(status) {
|
||||||
return {
|
return dispatch => {
|
||||||
type: REDRAFT,
|
dispatch({
|
||||||
status,
|
type: STATUS_EDIT,
|
||||||
raw_text,
|
status,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('COMPOSE'));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
export function deleteStatus(id, routerHistory) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
|
@ -156,11 +159,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||||
evictStatus(id);
|
evictStatus(id);
|
||||||
dispatch(deleteStatusSuccess(id));
|
dispatch(deleteStatusSuccess(id));
|
||||||
dispatch(deleteFromTimelines(id));
|
dispatch(deleteFromTimelines(id));
|
||||||
|
|
||||||
if (withRedraft) {
|
|
||||||
dispatch(redraft(status, response.data.text));
|
|
||||||
ensureComposeIsVisible(getState, routerHistory);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(deleteStatusFail(id, error));
|
dispatch(deleteStatusFail(id, error));
|
||||||
});
|
});
|
||||||
|
@ -272,7 +270,7 @@ export function muteStatusFail(id, error) {
|
||||||
export function unmuteStatus(id) {
|
export function unmuteStatus(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
dispatch(unmuteStatusRequest(id));
|
dispatch(unmuteStatusRequest(id));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
|
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { updateNotificationsQueue, expandNotifications } from './notifications';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
import { fetchFilters } from './filters';
|
import { fetchFilters } from './filters';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
|
import { handleComposeSubmit } from './compose';
|
||||||
|
|
||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
|
||||||
|
@ -61,3 +62,18 @@ export const connectHashtagStream = (id, tag, accept) => connectTimelineStream
|
||||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||||
export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
|
export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
|
||||||
|
|
||||||
|
export const connectStatusUpdateStream = () => {
|
||||||
|
return connectStream('statuscard', null, (dispatch, getState) => {
|
||||||
|
return {
|
||||||
|
onConnect() {},
|
||||||
|
onDisconnect() {},
|
||||||
|
onReceive (data) {
|
||||||
|
if (!data['event'] || !data['payload']) return;
|
||||||
|
if (data.event === 'update') {
|
||||||
|
handleComposeSubmit(dispatch, getState, {data: JSON.parse(data.payload)}, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -43,9 +43,9 @@ export default class ExtendedVideoPlayer extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='extended-video-player'>
|
<div className='extended-video-player'>
|
||||||
<video
|
<video
|
||||||
|
playsInline
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
src={src}
|
src={src}
|
||||||
autoPlay
|
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
aria-label={alt}
|
aria-label={alt}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { shortNumberFormat } from '../utils/numbers';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
members: { id: 'groups.card.members', defaultMessage: 'Members' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default
|
||||||
|
@injectIntl
|
||||||
|
class GroupListItem extends ImmutablePureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
group: ImmutablePropTypes.map.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, group } = this.props;
|
||||||
|
|
||||||
|
if (!group) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='trends__item'>
|
||||||
|
<div className='trends__item__name'>
|
||||||
|
<Link to={`/groups/${group.get('id')}`}>
|
||||||
|
<strong>{group.get('title')}</strong>
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
{shortNumberFormat(group.get('member_count'))}
|
||||||
|
|
||||||
|
{intl.formatMessage(messages.members)}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -7,9 +7,11 @@ import { decode } from 'blurhash';
|
||||||
import IconButton from '../icon_button';
|
import IconButton from '../icon_button';
|
||||||
import { isIOS } from '../../utils/is_mobile';
|
import { isIOS } from '../../utils/is_mobile';
|
||||||
import { autoPlayGif, displayMedia } from '../../initial_state';
|
import { autoPlayGif, displayMedia } from '../../initial_state';
|
||||||
|
import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio';
|
||||||
|
|
||||||
import './media_gallery.scss';
|
import './media_gallery.scss';
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||||
warning: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' },
|
warning: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' },
|
||||||
|
@ -26,6 +28,7 @@ class Item extends ImmutablePureComponent {
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
displayWidth: PropTypes.number,
|
displayWidth: PropTypes.number,
|
||||||
visible: PropTypes.bool.isRequired,
|
visible: PropTypes.bool.isRequired,
|
||||||
|
dimensions: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -51,9 +54,15 @@ class Item extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLoadedMetaData = (e) => {
|
||||||
|
if (!this.hoverToPlay()) {
|
||||||
|
e.target.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hoverToPlay () {
|
hoverToPlay () {
|
||||||
const { attachment } = this.props;
|
const { attachment } = this.props;
|
||||||
return !autoPlayGif && attachment.get('type') === 'gifv';
|
return autoPlayGif === false && attachment.get('type') === 'gifv';
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick = (e) => {
|
handleClick = (e) => {
|
||||||
|
@ -105,43 +114,36 @@ class Item extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { attachment, index, size, standalone, displayWidth, visible } = this.props;
|
const { attachment, index, size, standalone, displayWidth, visible, dimensions } = this.props;
|
||||||
|
|
||||||
const width = (size === 1) ? 100 : 50;
|
const ar = attachment.getIn(['meta', 'small', 'aspect']);
|
||||||
const height = (size === 4 || (size === 3 && index > 0)) ? 50 : 100;
|
|
||||||
|
|
||||||
let top = 'auto';
|
let width = 100;
|
||||||
let left = 'auto';
|
let height = '100%';
|
||||||
|
let top = 'auto';
|
||||||
|
let left = 'auto';
|
||||||
let bottom = 'auto';
|
let bottom = 'auto';
|
||||||
let right = 'auto';
|
let right = 'auto';
|
||||||
|
let float = 'left';
|
||||||
|
let position = 'relative';
|
||||||
|
|
||||||
switch(size) {
|
if (dimensions) {
|
||||||
case 2:
|
width = dimensions.w;
|
||||||
if (index === 0) right = '2px';
|
height = dimensions.h;
|
||||||
else left = '2px';
|
top = dimensions.t || 'auto';
|
||||||
break;
|
right = dimensions.r || 'auto';
|
||||||
case 3:
|
bottom = dimensions.b || 'auto';
|
||||||
if (index === 0) right = '2px';
|
left = dimensions.l || 'auto';
|
||||||
else if (index > 0) left = '2px';
|
float = dimensions.float || 'left';
|
||||||
|
position = dimensions.pos || 'relative';
|
||||||
if (index === 1) bottom = '2px';
|
|
||||||
else if (index > 1) top = '2px';
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
if (index === 0 || index === 2) right = '2px';
|
|
||||||
if (index === 1 || index === 3) left = '2px';
|
|
||||||
|
|
||||||
if (index < 2) bottom = '2px';
|
|
||||||
else top = '2px';
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let thumbnail = '';
|
let thumbnail = '';
|
||||||
|
|
||||||
if (attachment.get('type') === 'unknown') {
|
if (attachment.get('type') === 'unknown') {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('media-item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||||
<a className='media-item__thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
|
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
|
||||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -182,7 +184,7 @@ class Item extends ImmutablePureComponent {
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (attachment.get('type') === 'gifv') {
|
} else if (attachment.get('type') === 'gifv') {
|
||||||
const autoPlay = !isIOS() && autoPlayGif;
|
const autoPlay = !isIOS() && autoPlayGif !== false;
|
||||||
|
|
||||||
thumbnail = (
|
thumbnail = (
|
||||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||||
|
@ -195,9 +197,12 @@ class Item extends ImmutablePureComponent {
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
onLoadedMetadata={this.handleLoadedMetaData}
|
||||||
autoPlay={autoPlay}
|
autoPlay={autoPlay}
|
||||||
|
type='video/mp4'
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
|
playsInline
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className='media-gallery__gifv__label'>GIF</span>
|
<span className='media-gallery__gifv__label'>GIF</span>
|
||||||
|
@ -206,7 +211,7 @@ class Item extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('media-item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
|
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
|
||||||
{visible && thumbnail}
|
{visible && thumbnail}
|
||||||
</div>
|
</div>
|
||||||
|
@ -262,7 +267,7 @@ class MediaGallery extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRef = (node) => {
|
handleRef = (node) => {
|
||||||
if (node /*&& this.isStandaloneEligible()*/) {
|
if (node) {
|
||||||
// offsetWidth triggers a layout, so only calculate when we need to
|
// offsetWidth triggers a layout, so only calculate when we need to
|
||||||
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
||||||
|
|
||||||
|
@ -272,11 +277,6 @@ class MediaGallery extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isStandaloneEligible() {
|
|
||||||
const { media, standalone } = this.props;
|
|
||||||
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, intl, sensitive, height, defaultWidth } = this.props;
|
const { media, intl, sensitive, height, defaultWidth } = this.props;
|
||||||
const { visible } = this.state;
|
const { visible } = this.state;
|
||||||
|
@ -286,24 +286,221 @@ class MediaGallery extends PureComponent {
|
||||||
let children, spoilerButton;
|
let children, spoilerButton;
|
||||||
|
|
||||||
const style = {};
|
const style = {};
|
||||||
|
const size = media.take(4).size;
|
||||||
|
|
||||||
if (this.isStandaloneEligible()) {
|
const standard169 = width / (16 / 9);
|
||||||
if (width) {
|
const standard169_percent = 100 / (16 / 9);
|
||||||
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
|
const standard169_px = `${standard169}px`;
|
||||||
|
const panoSize = Math.floor(width / maximumAspectRatio);
|
||||||
|
const panoSize_px = `${Math.floor(width / maximumAspectRatio)}px`;
|
||||||
|
let itemsDimensions = [];
|
||||||
|
|
||||||
|
if (size == 1 && width) {
|
||||||
|
const aspectRatio = media.getIn([0, 'meta', 'small', 'aspect']);
|
||||||
|
|
||||||
|
if (isPanoramic(aspectRatio)) {
|
||||||
|
style.height = Math.floor(width / maximumAspectRatio);
|
||||||
|
} else if (isPortrait(aspectRatio)) {
|
||||||
|
style.height = Math.floor(width / minimumAspectRatio);
|
||||||
|
} else {
|
||||||
|
style.height = Math.floor(width / aspectRatio);
|
||||||
|
}
|
||||||
|
} else if (size > 1 && width) {
|
||||||
|
const ar1 = media.getIn([0, 'meta', 'small', 'aspect']);
|
||||||
|
const ar2 = media.getIn([1, 'meta', 'small', 'aspect']);
|
||||||
|
const ar3 = media.getIn([2, 'meta', 'small', 'aspect']);
|
||||||
|
const ar4 = media.getIn([3, 'meta', 'small', 'aspect']);
|
||||||
|
|
||||||
|
if (size == 2) {
|
||||||
|
if (isPortrait(ar1) && isPortrait(ar2)) {
|
||||||
|
style.height = width - (width / maximumAspectRatio);
|
||||||
|
} else if (isPanoramic(ar1) && isPanoramic(ar2)) {
|
||||||
|
style.height = panoSize * 2;
|
||||||
|
} else if (
|
||||||
|
(isPanoramic(ar1) && isPortrait(ar2)) ||
|
||||||
|
(isPortrait(ar1) && isPanoramic(ar2)) ||
|
||||||
|
(isPanoramic(ar1) && isNonConformingRatio(ar2)) ||
|
||||||
|
(isNonConformingRatio(ar1) && isPanoramic(ar2))
|
||||||
|
) {
|
||||||
|
style.height = (width * 0.6) + (width / maximumAspectRatio);
|
||||||
|
} else {
|
||||||
|
style.height = width / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
if (isPortrait(ar1) && isPortrait(ar2)) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: '100%', r: '2px' },
|
||||||
|
{ w: 50, h: '100%', l: '2px' }
|
||||||
|
];
|
||||||
|
} else if (isPanoramic(ar1) && isPanoramic(ar2)) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 100, h: panoSize_px, b: '2px' },
|
||||||
|
{ w: 100, h: panoSize_px, t: '2px' }
|
||||||
|
];
|
||||||
|
} else if (
|
||||||
|
(isPanoramic(ar1) && isPortrait(ar2)) ||
|
||||||
|
(isPanoramic(ar1) && isNonConformingRatio(ar2))
|
||||||
|
) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 100, h: `${(width / maximumAspectRatio)}px`, b: '2px' },
|
||||||
|
{ w: 100, h: `${(width * 0.6)}px`, t: '2px' },
|
||||||
|
];
|
||||||
|
} else if (
|
||||||
|
(isPortrait(ar1) && isPanoramic(ar2)) ||
|
||||||
|
(isNonConformingRatio(ar1) && isPanoramic(ar2))
|
||||||
|
) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 100, h: `${(width * 0.6)}px`, b: '2px' },
|
||||||
|
{ w: 100, h: `${(width / maximumAspectRatio)}px`, t: '2px' },
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: '100%', r: '2px' },
|
||||||
|
{ w: 50, h: '100%', l: '2px' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else if (size == 3) {
|
||||||
|
if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) {
|
||||||
|
style.height = panoSize * 3;
|
||||||
|
} else if (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) {
|
||||||
|
style.height = Math.floor(width / minimumAspectRatio);
|
||||||
|
} else {
|
||||||
|
style.height = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 100, h: `50%`, b: '2px' },
|
||||||
|
{ w: 50, h: '50%', t: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: '50%', t: '2px', l: '2px' }
|
||||||
|
];
|
||||||
|
} else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 100, h: panoSize_px, b: '4px' },
|
||||||
|
{ w: 100, h: panoSize_px },
|
||||||
|
{ w: 100, h: panoSize_px, t: '4px' }
|
||||||
|
];
|
||||||
|
} else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: `100%`, r: '2px' },
|
||||||
|
{ w: 50, h: '50%', b: '2px', l: '2px' },
|
||||||
|
{ w: 50, h: '50%', t: '2px', l: '2px' },
|
||||||
|
];
|
||||||
|
} else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: '50%', b: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none' },
|
||||||
|
{ w: 50, h: `100%`, r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none' }
|
||||||
|
];
|
||||||
|
} else if (
|
||||||
|
(isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) ||
|
||||||
|
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3))
|
||||||
|
) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: '50%', b: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: `100%`, l: '2px', float: 'right' },
|
||||||
|
{ w: 50, h: '50%', t: '2px', r: '2px' }
|
||||||
|
];
|
||||||
|
} else if (
|
||||||
|
(isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) ||
|
||||||
|
(isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3))
|
||||||
|
) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: panoSize_px, b: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: panoSize_px, b: '2px', l: '2px' },
|
||||||
|
{ w: 100, h: `${width - panoSize}px`, t: '2px' }
|
||||||
|
];
|
||||||
|
} else if (
|
||||||
|
(isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) ||
|
||||||
|
(isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3))
|
||||||
|
) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 100, h: `${width - panoSize}px`, b: '2px' },
|
||||||
|
{ w: 50, h: panoSize_px, t: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: panoSize_px, t: '2px', l: '2px' },
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: '50%', b: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: '50%', b: '2px', l: '2px' },
|
||||||
|
{ w: 100, h: `50%`, t: '2px' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else if (size == 4) {
|
||||||
|
if (
|
||||||
|
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) ||
|
||||||
|
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isNonConformingRatio(ar4)) ||
|
||||||
|
(isPortrait(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3) && isPortrait(ar4)) ||
|
||||||
|
(isPortrait(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3) && isPortrait(ar4)) ||
|
||||||
|
(isNonConformingRatio(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4))
|
||||||
|
) {
|
||||||
|
style.height = Math.floor(width / minimumAspectRatio);
|
||||||
|
} else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) {
|
||||||
|
style.height = panoSize * 2;
|
||||||
|
} else if (
|
||||||
|
(isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) ||
|
||||||
|
(isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4))
|
||||||
|
) {
|
||||||
|
style.height = panoSize + (width / 2);
|
||||||
|
} else {
|
||||||
|
style.height = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
if (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: panoSize_px, b: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: panoSize_px, b: '2px', l: '2px' },
|
||||||
|
{ w: 50, h: `${(width / 2)}px`, t: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: `${(width / 2)}px`, t: '2px', l: '2px' },
|
||||||
|
];
|
||||||
|
} else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: `${(width / 2)}px`, b: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: `${(width / 2)}px`, b: '2px', l: '2px' },
|
||||||
|
{ w: 50, h: panoSize_px, t: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: panoSize_px, t: '2px', l: '2px' },
|
||||||
|
];
|
||||||
|
} else if (
|
||||||
|
(isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) ||
|
||||||
|
(isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4))
|
||||||
|
) {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 67, h: '100%', r: '2px' },
|
||||||
|
{ w: 33, h: '33%', b: '4px', l: '2px' },
|
||||||
|
{ w: 33, h: '33%', l: '2px' },
|
||||||
|
{ w: 33, h: '33%', t: '4px', l: '2px' }
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
itemsDimensions = [
|
||||||
|
{ w: 50, h: '50%', b: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: '50%', b: '2px', l: '2px' },
|
||||||
|
{ w: 50, h: '50%', t: '2px', r: '2px' },
|
||||||
|
{ w: 50, h: '50%', t: '2px', l: '2px' },
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (width) {
|
|
||||||
style.height = width / (16/9);
|
|
||||||
} else {
|
} else {
|
||||||
style.height = height;
|
style.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = media.take(4).size;
|
children = media.take(4).map((attachment, i) => (
|
||||||
|
<Item
|
||||||
if (this.isStandaloneEligible()) {
|
key={attachment.get('id')}
|
||||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
onClick={this.handleClick}
|
||||||
} else {
|
attachment={attachment}
|
||||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible} />);
|
index={i}
|
||||||
}
|
size={size}
|
||||||
|
displayWidth={width}
|
||||||
|
visible={visible}
|
||||||
|
dimensions={itemsDimensions[i]}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
||||||
|
|
|
@ -20,11 +20,18 @@ export default class ActionsModal extends ImmutablePureComponent {
|
||||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { icon = null, text, meta = null, active = false, href = '#' } = action;
|
const { icon = null, text, meta = null, active = false, href = '#', isLogout } = action;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={`${text}-${i}`}>
|
<li key={`${text}-${i}`}>
|
||||||
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
|
<a
|
||||||
|
href={href}
|
||||||
|
rel='noopener'
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
data-index={i}
|
||||||
|
className={classNames({ active })}
|
||||||
|
data-method={isLogout ? 'delete' : null}
|
||||||
|
>
|
||||||
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
|
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
|
||||||
<div>
|
<div>
|
||||||
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
||||||
|
|
|
@ -10,6 +10,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
composeId: state.getIn(['compose', 'id']),
|
||||||
composeText: state.getIn(['compose', 'text']),
|
composeText: state.getIn(['compose', 'text']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ class ModalBase extends PureComponent {
|
||||||
onOpenModal: PropTypes.func.isRequired,
|
onOpenModal: PropTypes.func.isRequired,
|
||||||
onCancelReplyCompose: PropTypes.func.isRequired,
|
onCancelReplyCompose: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
composeId: PropTypes.string,
|
||||||
composeText: PropTypes.string,
|
composeText: PropTypes.string,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -50,9 +52,9 @@ class ModalBase extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOnClose = () => {
|
handleOnClose = () => {
|
||||||
const { onOpenModal, composeText, onClose, intl, type, onCancelReplyCompose } = this.props;
|
const { onOpenModal, composeText, composeId, onClose, intl, type, onCancelReplyCompose } = this.props;
|
||||||
|
|
||||||
if (composeText && type === 'COMPOSE') {
|
if (!composeId && composeText && type == 'COMPOSE') {
|
||||||
onOpenModal('CONFIRM', {
|
onOpenModal('CONFIRM', {
|
||||||
message: <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this status?' />,
|
message: <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this status?' />,
|
||||||
confirm: intl.formatMessage(messages.confirm),
|
confirm: intl.formatMessage(messages.confirm),
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
EmbedModal,
|
EmbedModal,
|
||||||
ListEditor,
|
ListEditor,
|
||||||
ListAdder,
|
ListAdder,
|
||||||
|
StatusRevisionModal,
|
||||||
} from '../../features/ui/util/async-components';
|
} from '../../features/ui/util/async-components';
|
||||||
|
|
||||||
const MODAL_COMPONENTS = {
|
const MODAL_COMPONENTS = {
|
||||||
|
@ -34,10 +35,12 @@ const MODAL_COMPONENTS = {
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
'LIST_EDITOR': ListEditor,
|
'LIST_EDITOR': ListEditor,
|
||||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||||
'LIST_ADDER':ListAdder,
|
'LIST_ADDER': ListAdder,
|
||||||
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
|
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
|
||||||
|
'STATUS_REVISION': StatusRevisionModal,
|
||||||
'COMPOSE': () => Promise.resolve({ default: ComposeModal }),
|
'COMPOSE': () => Promise.resolve({ default: ComposeModal }),
|
||||||
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
|
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
|
||||||
|
'PRO_UPGRADE': () => Promise.resolve({ default: ProUpgradeModal }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends PureComponent {
|
export default class ModalRoot extends PureComponent {
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Avatar from './avatar';
|
||||||
|
import IconButton from './icon_button';
|
||||||
|
import Icon from './icon';
|
||||||
|
import DisplayName from './display_name';
|
||||||
|
import { closeSidebar } from '../actions/sidebar';
|
||||||
|
import { shortNumberFormat } from '../utils/numbers';
|
||||||
|
import { me } from '../initial_state';
|
||||||
|
import { makeGetAccount } from '../selectors';
|
||||||
|
import ProgressPanel from '../features/ui/components/progress_panel';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
followers: { id: 'account.followers', defaultMessage: 'Followers' },
|
||||||
|
follows: { id: 'account.follows', defaultMessage: 'Follows' },
|
||||||
|
profile: { id: 'account.profile', defaultMessage: 'Profile' },
|
||||||
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
|
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||||
|
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||||
|
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||||
|
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||||
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
|
lists: { id: 'column.lists', defaultMessage: 'Lists', },
|
||||||
|
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
|
||||||
|
more: { id: 'sidebar.more', defaultMessage: 'More' },
|
||||||
|
pro: { id: 'promo.gab_pro', defaultMessage: 'Upgrade to GabPRO' },
|
||||||
|
trends: { id: 'promo.trends', defaultMessage: 'Trends' },
|
||||||
|
search: { id: 'tabs_bar.search', defaultMessage: 'Search' },
|
||||||
|
shop: { id: 'tabs_bar.shop', defaultMessage: 'Store - Buy Merch' },
|
||||||
|
donate: { id: 'tabs_bar.donate', defaultMessage: 'Make a Donation' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
account: getAccount(state, me),
|
||||||
|
sidebarOpen: state.get('sidebar').sidebarOpen,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onClose () {
|
||||||
|
dispatch(closeSidebar());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
class SidebarMenu extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
|
sidebarOpen: PropTypes.bool,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
moreOpen: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
if (this.props.sidebarOpen) {
|
||||||
|
document.body.classList.add('with-modals--active');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('with-modals--active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMore = () => {
|
||||||
|
this.setState({
|
||||||
|
moreOpen: !this.state.moreOpen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSidebarClose = () => {
|
||||||
|
this.props.onClose();
|
||||||
|
this.setState({
|
||||||
|
moreOpen: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { sidebarOpen, intl, account } = this.props;
|
||||||
|
const { moreOpen } = this.state;
|
||||||
|
|
||||||
|
if (!me || !account) return null;
|
||||||
|
|
||||||
|
const acct = account.get('acct');
|
||||||
|
const isPro = account.get('is_pro');
|
||||||
|
|
||||||
|
const classes = classNames('sidebar-menu__root', {
|
||||||
|
'sidebar-menu__root--visible': sidebarOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
const moreIcon = moreOpen ? 'minus' : 'plus';
|
||||||
|
const moreContainerStyle = { display: moreOpen ? 'block' : 'none' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
<div className='sidebar-menu__wrapper' role='button' onClick={this.handleSidebarClose} />
|
||||||
|
<div className='sidebar-menu'>
|
||||||
|
|
||||||
|
<div className='sidebar-menu-header'>
|
||||||
|
<span className='sidebar-menu-header__title'>Account Info</span>
|
||||||
|
<IconButton title='close' onClick={this.handleSidebarClose} icon='close' className='sidebar-menu-header__btn' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sidebar-menu__content'>
|
||||||
|
|
||||||
|
<div className='sidebar-menu-profile'>
|
||||||
|
<div className='sidebar-menu-profile__avatar'>
|
||||||
|
<Link to={`/${acct}`} title={acct} onClick={this.handleSidebarClose}>
|
||||||
|
<Avatar account={account} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className='sidebar-menu-profile__name'>
|
||||||
|
<DisplayName account={account}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sidebar-menu-profile__stats'>
|
||||||
|
<NavLink className='sidebar-menu-profile-stat' to={`/${acct}/followers`} onClick={this.handleSidebarClose} title={intl.formatNumber(account.get('followers_count'))}>
|
||||||
|
<strong className='sidebar-menu-profile-stat__value'>{shortNumberFormat(account.get('followers_count'))}</strong>
|
||||||
|
<span className='sidebar-menu-profile-stat__label'>{intl.formatMessage(messages.followers)}</span>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink className='sidebar-menu-profile-stat' to={`/${acct}/following`} onClick={this.handleSidebarClose} title={intl.formatNumber(account.get('following_count'))}>
|
||||||
|
<strong className='sidebar-menu-profile-stat__value'>{shortNumberFormat(account.get('following_count'))}</strong>
|
||||||
|
<span className='sidebar-menu-profile-stat__label'>{intl.formatMessage(messages.follows)}</span>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sidebar-menu__section'>
|
||||||
|
<ProgressPanel />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sidebar-menu__section sidebar-menu__section--borderless'>
|
||||||
|
<NavLink className='sidebar-menu-item' to={`/${acct}`} onClick={this.handleSidebarClose}>
|
||||||
|
<Icon id='user' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.profile)}</span>
|
||||||
|
</NavLink>
|
||||||
|
{
|
||||||
|
!isPro &&
|
||||||
|
<a className='sidebar-menu-item' href='https://pro.gab.com'>
|
||||||
|
<Icon id='arrow-up' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.pro)}</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<a className='sidebar-menu-item' href='https://shop.dissenter.com/category/donations'>
|
||||||
|
<Icon id='heart' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate)}</span>
|
||||||
|
</a>
|
||||||
|
<a className='sidebar-menu-item' href='https://shop.dissenter.com'>
|
||||||
|
<Icon id='shopping-cart' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.shop)}</span>
|
||||||
|
</a>
|
||||||
|
<a className='sidebar-menu-item' href='https://trends.gab.com'>
|
||||||
|
<Icon id='signal' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.trends)}</span>
|
||||||
|
</a>
|
||||||
|
<NavLink className='sidebar-menu-item' to='/search' onClick={this.handleSidebarClose}>
|
||||||
|
<Icon id='search' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.search)}</span>
|
||||||
|
</NavLink>
|
||||||
|
<a className='sidebar-menu-item' href='/settings/preferences'>
|
||||||
|
<Icon id='cog' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.preferences)}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sidebar-menu__section'>
|
||||||
|
<div className='sidebar-menu-item' onClick={this.toggleMore} role='button'>
|
||||||
|
<Icon id={moreIcon} fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.more)}</span>
|
||||||
|
</div>
|
||||||
|
<div style={moreContainerStyle}>
|
||||||
|
<NavLink className='sidebar-menu-item' to='/lists' onClick={this.handleSidebarClose}>
|
||||||
|
<Icon id='list' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.lists)}</span>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink className='sidebar-menu-item' to='/follow_requests' onClick={this.handleSidebarClose}>
|
||||||
|
<Icon id='user-plus' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.follow_requests)}</span>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink className='sidebar-menu-item' to='/blocks' onClick={this.handleSidebarClose}>
|
||||||
|
<Icon id='ban' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.blocks)}</span>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink className='sidebar-menu-item' to='/domain_blocks' onClick={this.handleSidebarClose}>
|
||||||
|
<Icon id='sitemap' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.domain_blocks)}</span>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink className='sidebar-menu-item' to='/mutes' onClick={this.handleSidebarClose}>
|
||||||
|
<Icon id='times-circle' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.mutes)}</span>
|
||||||
|
</NavLink>
|
||||||
|
<a className='sidebar-menu-item' href='/filters'>
|
||||||
|
<Icon id='filter' fixedWidth />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.filters)}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sidebar-menu__section'>
|
||||||
|
<a className='sidebar-menu-item' href='/auth/sign_out' data-method='delete'>
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.logout)}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,18 @@
|
||||||
|
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Avatar from './avatar';
|
||||||
|
import AvatarOverlay from './avatar_overlay';
|
||||||
|
import AvatarComposite from './avatar_composite';
|
||||||
|
import RelativeTimestamp from './relative_timestamp';
|
||||||
|
import DisplayName from './display_name';
|
||||||
|
import StatusContent from './status_content';
|
||||||
|
import StatusQuote from './status_quote';
|
||||||
|
import StatusActionBar from './status_action_bar';
|
||||||
|
import AttachmentList from './attachment_list';
|
||||||
|
import Card from '../features/status/components/card';
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
@ -63,9 +77,12 @@ class Status extends ImmutablePureComponent {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
|
onShowRevisions: PropTypes.func,
|
||||||
|
onQuote: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
|
onEdit: PropTypes.func,
|
||||||
onDirect: PropTypes.func,
|
onDirect: PropTypes.func,
|
||||||
onMention: PropTypes.func,
|
onMention: PropTypes.func,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
|
@ -86,6 +103,8 @@ class Status extends ImmutablePureComponent {
|
||||||
cacheMediaWidth: PropTypes.func,
|
cacheMediaWidth: PropTypes.func,
|
||||||
cachedMediaWidth: PropTypes.number,
|
cachedMediaWidth: PropTypes.number,
|
||||||
group: ImmutablePropTypes.map,
|
group: ImmutablePropTypes.map,
|
||||||
|
promoted: PropTypes.bool,
|
||||||
|
onOpenProUpgradeModal: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
|
@ -248,11 +267,16 @@ class Status extends ImmutablePureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
|
handleOpenProUpgradeModal = () => {
|
||||||
|
this.props.onOpenProUpgradeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
let media = null;
|
let media = null;
|
||||||
let statusAvatar, prepend, rebloggedByText, reblogContent;
|
let statusAvatar, prepend, rebloggedByText, reblogContent;
|
||||||
|
|
||||||
const { intl, hidden, featured, unread, showThread, group } = this.props;
|
const { intl, hidden, featured, otherAccounts, unread, showThread, group, promoted } = this.props;
|
||||||
|
|
||||||
let { status, account, ...other } = this.props;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
|
@ -284,7 +308,14 @@ class Status extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featured) {
|
if (promoted) {
|
||||||
|
prepend = (
|
||||||
|
<button className='status__prepend status__prepend--promoted' onClick={this.handleOpenProUpgradeModal}>
|
||||||
|
<div className='status__prepend-icon-wrapper'><Icon id='star' className='status__prepend-icon' fixedWidth /></div>
|
||||||
|
<FormattedMessage id='status.promoted' defaultMessage='Promoted gab' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else if (featured) {
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'>
|
<div className='status__prepend-icon-wrapper'>
|
||||||
|
@ -341,6 +372,7 @@ class Status extends ImmutablePureComponent {
|
||||||
blurhash={video.get('blurhash')}
|
blurhash={video.get('blurhash')}
|
||||||
src={video.get('url')}
|
src={video.get('url')}
|
||||||
alt={video.get('description')}
|
alt={video.get('description')}
|
||||||
|
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||||
width={this.props.cachedMediaWidth}
|
width={this.props.cachedMediaWidth}
|
||||||
height={110}
|
height={110}
|
||||||
inline
|
inline
|
||||||
|
@ -376,7 +408,6 @@ class Status extends ImmutablePureComponent {
|
||||||
<Card
|
<Card
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
onOpenMedia={this.props.onOpenMedia}
|
||||||
card={status.get('card')}
|
card={status.get('card')}
|
||||||
compact
|
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
defaultWidth={this.props.cachedMediaWidth}
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
/>
|
/>
|
||||||
|
@ -446,10 +477,10 @@ class Status extends ImmutablePureComponent {
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!group && status.get('group') && (
|
{((!group && status.get('group')) || status.get('revised_at') !== null) && (
|
||||||
<div className='status__meta'>
|
<div className='status__meta'>
|
||||||
Posted in{' '}
|
{!group && status.get('group') && <React.Fragment>Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink></React.Fragment>}
|
||||||
<NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink>
|
{status.get('revised_at') !== null && <a onClick={() => other.onShowRevisions(status)}> Edited</a>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -464,9 +495,11 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
{showThread &&
|
{status.get('quote') && <StatusQuote
|
||||||
status.get('in_reply_to_id') &&
|
id={status.get('quote')}
|
||||||
status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
/>}
|
||||||
|
|
||||||
|
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||||
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
||||||
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -12,18 +12,20 @@ import './status_action_bar.scss';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||||
|
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
|
||||||
more: { id: 'status.more', defaultMessage: 'More' },
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
||||||
|
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
|
||||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
||||||
|
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
||||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||||
|
@ -57,6 +59,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
onOpenUnauthorizedModal: PropTypes.func.isRequired,
|
onOpenUnauthorizedModal: PropTypes.func.isRequired,
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
|
onQuote: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
|
@ -87,13 +90,12 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShareClick = () => {
|
handleQuoteClick = () => {
|
||||||
navigator.share({
|
if (me) {
|
||||||
text: this.props.status.get('search_index'),
|
this.props.onQuote(this.props.status, this.context.router.history);
|
||||||
url: this.props.status.get('url'),
|
} else {
|
||||||
}).catch((e) => {
|
this.props.onOpenUnauthorizedModal();
|
||||||
if (e.name !== 'AbortError') console.error(e);
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFavouriteClick = () => {
|
handleFavouriteClick = () => {
|
||||||
|
@ -116,8 +118,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history);
|
this.props.onDelete(this.props.status, this.context.router.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRedraftClick = () => {
|
handleEditClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onEdit(this.props.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePinClick = () => {
|
handlePinClick = () => {
|
||||||
|
@ -215,9 +217,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
menu.push({ text: formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
|
menu.push({ text: formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: formatMessage(messages.redraft), action: this.handleRedraftClick });
|
|
||||||
} else {
|
} else {
|
||||||
menu.push({ text: formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
menu.push({ text: formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
@ -290,19 +291,13 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
{reblogCount !== 0 && <Link to={`/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reblogs`} className='status-action-bar-item__link'>{reblogCount}</Link>}
|
{reblogCount !== 0 && <Link to={`/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reblogs`} className='status-action-bar-item__link'>{reblogCount}</Link>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='status-action-bar-item'>
|
<div className='status__action-bar__counter'>
|
||||||
<IconButton
|
<IconButton className='status__action-bar-button' disabled={!publicStatus} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-left' onClick={this.handleQuoteClick} />
|
||||||
className='status-action-bar-item__btn star-icon'
|
</div>
|
||||||
active={status.get('favourited')}
|
<div className='status__action-bar__counter'>
|
||||||
pressed={status.get('favourited')}
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
title={formatMessage(messages.favourite)}
|
{favoriteCount !== 0 && <span className='detailed-status__link'>{favoriteCount}</span>}
|
||||||
icon='star'
|
|
||||||
onClick={this.handleFavouriteClick}
|
|
||||||
/>
|
|
||||||
{favoriteCount !== 0 && <span className='status-action-bar-item__link'>{favoriteCount}</span>}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{shareButton}
|
|
||||||
|
|
||||||
<div className='status-action-bar__dropdown'>
|
<div className='status-action-bar__dropdown'>
|
||||||
<DropdownMenuContainer
|
<DropdownMenuContainer
|
||||||
|
|
|
@ -53,6 +53,7 @@ class StatusCheckBox extends ImmutablePureComponent {
|
||||||
blurhash={video.get('blurhash')}
|
blurhash={video.get('blurhash')}
|
||||||
src={video.get('url')}
|
src={video.get('url')}
|
||||||
alt={video.get('description')}
|
alt={video.get('description')}
|
||||||
|
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||||
width={239}
|
width={239}
|
||||||
height={110}
|
height={110}
|
||||||
inline
|
inline
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Icon from '../icon';
|
||||||
|
|
||||||
import './status_content.scss';
|
import './status_content.scss';
|
||||||
|
|
||||||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 200;
|
||||||
|
|
||||||
export default class StatusContent extends ImmutablePureComponent {
|
export default class StatusContent extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export default class StatusContent extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
link.classList.add('status-link');
|
link.classList.add('status-link');
|
||||||
|
|
||||||
let mention = this.props.status.get('mentions').find(item => link.href === `/${item.get('acct')}`);
|
let mention = this.props.status.get('mentions').find(item => link.href === `${item.get('url')}`);
|
||||||
|
|
||||||
if (mention) {
|
if (mention) {
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||||
|
|
|
@ -26,12 +26,24 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
withGroupAdmin: PropTypes.bool,
|
withGroupAdmin: PropTypes.bool,
|
||||||
onScrollToTop: PropTypes.func,
|
onScrollToTop: PropTypes.func,
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
promotion: PropTypes.object,
|
||||||
|
promotedStatus: ImmutablePropTypes.map,
|
||||||
|
fetchStatus: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.handleDequeueTimeline();
|
this.handleDequeueTimeline();
|
||||||
|
this.fetchPromotedStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetchPromotedStatus() {
|
||||||
|
const { promotion, promotedStatus, fetchStatus } = this.props;
|
||||||
|
|
||||||
|
if (promotion && !promotedStatus) {
|
||||||
|
fetchStatus(promotion.status_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getFeaturedStatusCount = () => {
|
getFeaturedStatusCount = () => {
|
||||||
return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
|
return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
|
||||||
}
|
}
|
||||||
|
@ -84,30 +96,23 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, ...other } = this.props;
|
const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, promotion, promotedStatus, ...other } = this.props;
|
||||||
|
|
||||||
if (isPartial) {
|
if (isPartial) {
|
||||||
return ( <ColumnIndicator type='loading' /> );
|
return ( <ColumnIndicator type='loading' /> );
|
||||||
}
|
}
|
||||||
|
|
||||||
let scrollableContent = null;
|
let scrollableContent = (isLoading || statusIds.size > 0) ? (
|
||||||
if (isLoading || statusIds.size > 0) {
|
statusIds.map((statusId, index) => statusId === null ? (
|
||||||
scrollableContent = statusIds.map((statusId, i) => {
|
<LoadGap
|
||||||
if (statusId === null) {
|
key={'gap:' + statusIds.get(index + 1)}
|
||||||
return (
|
disabled={isLoading}
|
||||||
<LoadMore
|
maxId={index > 0 ? statusIds.get(index - 1) : null}
|
||||||
gap
|
onClick={onLoadMore}
|
||||||
key={'gap:' + statusIds.get(i + 1)}
|
/>
|
||||||
disabled={isLoading}
|
) : (
|
||||||
maxId={i > 0 ? statusIds.get(i - 1) : null}
|
<React.Fragment key={statusId}>
|
||||||
onClick={onLoadMore}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
key={statusId}
|
|
||||||
id={statusId}
|
id={statusId}
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
|
@ -116,9 +121,17 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
withGroupAdmin={withGroupAdmin}
|
withGroupAdmin={withGroupAdmin}
|
||||||
showThread
|
showThread
|
||||||
/>
|
/>
|
||||||
);
|
{promotedStatus && index === promotion.position && (
|
||||||
});
|
<StatusContainer
|
||||||
}
|
id={promotion.status_id}
|
||||||
|
contextType={timelineId}
|
||||||
|
promoted
|
||||||
|
showThread
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
) : null;
|
||||||
|
|
||||||
if (scrollableContent && featuredStatusIds) {
|
if (scrollableContent && featuredStatusIds) {
|
||||||
scrollableContent = featuredStatusIds.map(statusId => (
|
scrollableContent = featuredStatusIds.map(statusId => (
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import StatusContent from './status_content';
|
||||||
|
import DisplayName from './display_name';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
status: state.getIn(['statuses', id]),
|
||||||
|
account: state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||||
|
});
|
||||||
|
|
||||||
|
@connect(mapStateToProps)
|
||||||
|
export default class StatusQuote extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, account } = this.props;
|
||||||
|
|
||||||
|
const statusUrl = `/${account.get('acct')}/posts/${status.get('id')}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink to={statusUrl} className="status__quote">
|
||||||
|
<DisplayName account={account} />
|
||||||
|
<StatusContent
|
||||||
|
status={status}
|
||||||
|
expanded={false}
|
||||||
|
onClick
|
||||||
|
collapsable
|
||||||
|
/>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,7 +9,10 @@ import UI from '../features/ui';
|
||||||
import Introduction from '../features/introduction';
|
import Introduction from '../features/introduction';
|
||||||
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||||
import { hydrateStore } from '../actions/store';
|
import { hydrateStore } from '../actions/store';
|
||||||
import { connectUserStream } from '../actions/streaming';
|
import {
|
||||||
|
connectUserStream,
|
||||||
|
connectStatusUpdateStream,
|
||||||
|
} from '../actions/streaming';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
import initialState from '../initial_state';
|
import initialState from '../initial_state';
|
||||||
|
@ -68,6 +71,7 @@ export default class GabSocial extends PureComponent {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.disconnect = store.dispatch(connectUserStream());
|
this.disconnect = store.dispatch(connectUserStream());
|
||||||
|
store.dispatch(connectStatusUpdateStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
|
quoteCompose,
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
import {
|
import {
|
||||||
reblog,
|
reblog,
|
||||||
|
@ -18,6 +19,7 @@ import {
|
||||||
muteStatus,
|
muteStatus,
|
||||||
unmuteStatus,
|
unmuteStatus,
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
|
editStatus,
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
|
@ -35,11 +37,11 @@ import {
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
|
||||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' },
|
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
|
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
|
||||||
|
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -70,6 +72,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onQuote (status, router) {
|
||||||
|
dispatch((_, getState) => {
|
||||||
|
let state = getState();
|
||||||
|
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: intl.formatMessage(messages.quoteMessage),
|
||||||
|
confirm: intl.formatMessage(messages.quoteConfirm),
|
||||||
|
onConfirm: () => dispatch(quoteCompose(status, router)),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(quoteCompose(status, router));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onModalReblog (status) {
|
onModalReblog (status) {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
|
@ -86,6 +103,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onShowRevisions (status) {
|
||||||
|
dispatch(openModal('STATUS_REVISION', { status }));
|
||||||
|
},
|
||||||
|
|
||||||
onFavourite (status) {
|
onFavourite (status) {
|
||||||
if (status.get('favourited')) {
|
if (status.get('favourited')) {
|
||||||
dispatch(unfavourite(status));
|
dispatch(unfavourite(status));
|
||||||
|
@ -109,18 +130,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
onDelete (status, history, withRedraft = false) {
|
onDelete (status, history) {
|
||||||
if (!deleteModal) {
|
if (!deleteModal) {
|
||||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
dispatch(deleteStatus(status.get('id'), history));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
message: intl.formatMessage(messages.deleteMessage),
|
||||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
onConfirm: () => dispatch(deleteStatus(status.get('id'), history)),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onEdit (status) {
|
||||||
|
dispatch(editStatus(status));
|
||||||
|
},
|
||||||
|
|
||||||
onDirect (account, router) {
|
onDirect (account, router) {
|
||||||
dispatch(directCompose(account, router));
|
dispatch(directCompose(account, router));
|
||||||
},
|
},
|
||||||
|
@ -183,6 +208,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(groupRemoveStatus(groupId, statusId));
|
dispatch(groupRemoveStatus(groupId, statusId));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onOpenProUpgradeModal() {
|
||||||
|
dispatch(openModal('PRO_UPGRADE'));
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { me } from '../initial_state';
|
import { me, promotions } from '../initial_state';
|
||||||
import StatusList from '../components/status_list/status_list';
|
|
||||||
import { dequeueTimeline } from 'gabsocial/actions/timelines';
|
import { dequeueTimeline } from 'gabsocial/actions/timelines';
|
||||||
import { scrollTopTimeline } from '../actions/timelines';
|
import { scrollTopTimeline } from '../actions/timelines';
|
||||||
|
import { sample } from 'lodash';
|
||||||
|
import { fetchStatus } from '../actions/statuses';
|
||||||
|
|
||||||
const makeGetStatusIds = () => createSelector([
|
const makeGetStatusIds = () => createSelector([
|
||||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
(state, { type, id }) => state.getIn(['settings', type], ImmutableMap()),
|
||||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
(state, { type, id }) => state.getIn(['timelines', id, 'items'], ImmutableList()),
|
||||||
(state) => state.get('statuses'),
|
(state) => state.get('statuses'),
|
||||||
], (columnSettings, statusIds, statuses) => {
|
], (columnSettings, statusIds, statuses) => {
|
||||||
return statusIds.filter(id => {
|
return statusIds.filter(id => {
|
||||||
|
@ -31,13 +32,16 @@ const makeGetStatusIds = () => createSelector([
|
||||||
|
|
||||||
const mapStateToProps = (state, {timelineId}) => {
|
const mapStateToProps = (state, {timelineId}) => {
|
||||||
const getStatusIds = makeGetStatusIds();
|
const getStatusIds = makeGetStatusIds();
|
||||||
|
const promotion = promotions.length > 0 && sample(promotions.filter(p => p.timeline_id === timelineId));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusIds: getStatusIds(state, { type: timelineId }),
|
statusIds: getStatusIds(state, { type: timelineId.substring(0,5) === 'group' ? 'group' : timelineId, id: timelineId }),
|
||||||
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
||||||
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||||
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
||||||
totalQueuedItemsCount: state.getIn(['timelines', timelineId, 'totalQueuedItemsCount']),
|
totalQueuedItemsCount: state.getIn(['timelines', timelineId, 'totalQueuedItemsCount']),
|
||||||
|
promotion: promotion,
|
||||||
|
promotedStatus: promotion && state.getIn(['statuses', promotion.status_id])
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,6 +55,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||||
onScroll: debounce(() => {
|
onScroll: debounce(() => {
|
||||||
dispatch(scrollTopTimeline(ownProps.timelineId, false));
|
dispatch(scrollTopTimeline(ownProps.timelineId, false));
|
||||||
}, 100),
|
}, 100),
|
||||||
|
fetchStatus(id) {
|
||||||
|
dispatch(fetchStatus(id));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
|
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
|
||||||
|
|
|
@ -5,9 +5,11 @@ import { autoPlayGif, displayMedia } from 'gabsocial/initial_state';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { decode } from 'blurhash';
|
import { decode } from 'blurhash';
|
||||||
import { isIOS } from 'gabsocial/utils/is_mobile';
|
import { isIOS } from 'gabsocial/utils/is_mobile';
|
||||||
|
import conversations_list_container from '../../direct_timeline/containers/conversations_list_container';
|
||||||
|
|
||||||
import './media_item.scss';
|
import './media_item.scss';
|
||||||
|
|
||||||
|
|
||||||
export default class MediaItem extends ImmutablePureComponent {
|
export default class MediaItem extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -125,8 +127,10 @@ export default class MediaItem extends ImmutablePureComponent {
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
autoPlay={autoPlay}
|
autoPlay={autoPlay}
|
||||||
|
preload='auto'
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
|
playsInline
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className='media-gallery__gifv__label'>GIF</span>
|
<span className='media-gallery__gifv__label'>GIF</span>
|
||||||
|
|
|
@ -44,7 +44,7 @@ class ActionBar extends PureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||||
menu.push({ text: intl.formatMessage(messages.filters), to: '/filters' });
|
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick });
|
menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
||||||
|
|
|
@ -18,9 +18,13 @@ import { isMobile } from '../../../../utils/is_mobile';
|
||||||
import { countableText } from '../../util/counter';
|
import { countableText } from '../../util/counter';
|
||||||
import Icon from '../../../../components/icon';
|
import Icon from '../../../../components/icon';
|
||||||
import Button from '../../../../components/button';
|
import Button from '../../../../components/button';
|
||||||
|
import SchedulePostDropdownContainer from '../containers/schedule_post_dropdown_container';
|
||||||
|
import UploadFormContainer from '../containers/upload_form_container';
|
||||||
|
import QuotedStatusPreviewContainer from '../containers/quoted_status_preview_container';
|
||||||
|
|
||||||
import './compose_form.scss';
|
import './compose_form.scss';
|
||||||
|
|
||||||
|
|
||||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||||
const maxPostCharacterCount = 3000;
|
const maxPostCharacterCount = 3000;
|
||||||
|
|
||||||
|
@ -29,6 +33,7 @@ const messages = defineMessages({
|
||||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Gab' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Gab' },
|
||||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||||
|
schedulePost: { id: 'compose_form.schedule_post', defaultMessage: 'Schedule Post' }
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -44,6 +49,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
edit: PropTypes.bool.isRequired,
|
||||||
text: PropTypes.string.isRequired,
|
text: PropTypes.string.isRequired,
|
||||||
suggestions: ImmutablePropTypes.list,
|
suggestions: ImmutablePropTypes.list,
|
||||||
spoiler: PropTypes.bool,
|
spoiler: PropTypes.bool,
|
||||||
|
@ -69,6 +75,8 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
group: ImmutablePropTypes.map,
|
group: ImmutablePropTypes.map,
|
||||||
isModalOpen: PropTypes.bool,
|
isModalOpen: PropTypes.bool,
|
||||||
|
scheduledAt: PropTypes.instanceOf(Date),
|
||||||
|
setScheduledAt: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -93,12 +101,21 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleClick = (e) => {
|
handleClick = (e) => {
|
||||||
if (!this.form) return false;
|
if (!this.form) return false;
|
||||||
|
if (e.target) {
|
||||||
|
if (e.target.classList.contains('react-datepicker__time-list-item')) return;
|
||||||
|
}
|
||||||
if (!this.form.contains(e.target)) {
|
if (!this.form.contains(e.target)) {
|
||||||
this.handleClickOutside();
|
this.handleClickOutside();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickOutside = () => {
|
handleClickOutside = () => {
|
||||||
|
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.setState({
|
this.setState({
|
||||||
composeFocused: false,
|
composeFocused: false,
|
||||||
});
|
});
|
||||||
|
@ -198,7 +215,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen } = this.props;
|
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen, quoteOfId, edit, scheduledAt } = this.props;
|
||||||
const condensed = shouldCondense && !this.props.text && !this.state.composeFocused;
|
const condensed = shouldCondense && !this.props.text && !this.state.composeFocused;
|
||||||
const disabled = this.props.isSubmitting;
|
const disabled = this.props.isSubmitting;
|
||||||
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||||
|
@ -213,6 +230,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scheduledAt) {
|
||||||
|
publishText = intl.formatMessage(messages.schedulePost);
|
||||||
|
}
|
||||||
|
|
||||||
const composeClassNames = classNames({
|
const composeClassNames = classNames({
|
||||||
'compose-form': true,
|
'compose-form': true,
|
||||||
'condensed': condensed,
|
'condensed': condensed,
|
||||||
|
@ -265,20 +286,25 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
{
|
{
|
||||||
!condensed &&
|
!condensed &&
|
||||||
<div className='compose-form__modifiers'>
|
<div className='compose-form__modifiers'>
|
||||||
<UploadForm />
|
<UploadFormContainer />
|
||||||
<PollFormContainer />
|
{!edit && <PollFormContainer />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</AutosuggestTextbox>
|
</AutosuggestTextbox>
|
||||||
|
|
||||||
|
{quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} />}
|
||||||
|
|
||||||
{
|
{
|
||||||
!condensed &&
|
!condensed &&
|
||||||
<div className='compose-form__buttons-wrapper'>
|
<div className='compose-form__buttons-wrapper'>
|
||||||
<div className='compose-form__buttons'>
|
<div className='compose-form__buttons'>
|
||||||
<UploadButtonContainer />
|
<UploadButtonContainer />
|
||||||
<PollButtonContainer />
|
{!edit && <PollButtonContainer />}
|
||||||
<PrivacyDropdownContainer />
|
<PrivacyDropdownContainer />
|
||||||
<SpoilerButtonContainer />
|
<SpoilerButtonContainer />
|
||||||
|
<SchedulePostDropdownContainer
|
||||||
|
position={isModalOpen ? 'top' : undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CharacterCounter max={maxPostCharacterCount} text={text} />
|
<CharacterCounter max={maxPostCharacterCount} text={text} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import DisplayName from '../../../components/display_name';
|
||||||
|
import StatusContent from '../../../components/status_content';
|
||||||
|
|
||||||
|
export default class QuotedStatusPreview extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
status: ImmutablePropTypes.map,
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, account } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='compose-form__quote-preview'>
|
||||||
|
<DisplayName account={account} />
|
||||||
|
|
||||||
|
<StatusContent
|
||||||
|
status={status}
|
||||||
|
expanded={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import DatePicker from 'react-datepicker';
|
||||||
|
import IconButton from '../../../components/icon_button';
|
||||||
|
import { isMobile } from '../../../is_mobile';
|
||||||
|
|
||||||
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
schedule_status: { id: 'schedule_status.title', defaultMessage: 'Schedule Status' },
|
||||||
|
});
|
||||||
|
|
||||||
|
class DatePickerWrapper extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { value, onClick } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className="schedule-post-dropdown-wrapper" onClick={onClick}>
|
||||||
|
{value}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class SchedulePostDropdown extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
date: PropTypes.instanceOf(Date),
|
||||||
|
setScheduledAt: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
isPro: PropTypes.bool,
|
||||||
|
onOpenProUpgradeModal: PropTypes.func.isRequired,
|
||||||
|
position: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleToggle = () => {
|
||||||
|
if (!this.props.isPro) {
|
||||||
|
return this.props.onOpenProUpgradeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { date } = this.props;
|
||||||
|
const value = date ? null : new Date();
|
||||||
|
this.handleSetDate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSetDate = (date) => {
|
||||||
|
this.props.setScheduledAt(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, date, isPro, position } = this.props;
|
||||||
|
|
||||||
|
const open = !!date;
|
||||||
|
const datePickerDisabled = !isPro;
|
||||||
|
const withPortal = isMobile(window.innerWidth);
|
||||||
|
const popperPlacement = position || undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='schedule-post-dropdown'>
|
||||||
|
<div className='schedule-post-dropdown__container'>
|
||||||
|
<IconButton
|
||||||
|
inverted
|
||||||
|
className='schedule-post-dropdown__icon'
|
||||||
|
icon='calendar'
|
||||||
|
title={intl.formatMessage(messages.schedule_status)}
|
||||||
|
size={18}
|
||||||
|
expanded={open}
|
||||||
|
active={open}
|
||||||
|
onClick={this.handleToggle}
|
||||||
|
style={{ height: null, lineHeight: '27px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
open &&
|
||||||
|
<DatePicker
|
||||||
|
target={this}
|
||||||
|
className='schedule-post-dropdown__datepicker'
|
||||||
|
minDate={new Date()}
|
||||||
|
selected={date}
|
||||||
|
onChange={date => this.handleSetDate(date)}
|
||||||
|
timeFormat="p"
|
||||||
|
timeIntervals={15}
|
||||||
|
timeCaption="Time"
|
||||||
|
dateFormat="MMM d, yyyy h:mm aa"
|
||||||
|
disabled={datePickerDisabled}
|
||||||
|
showTimeSelect
|
||||||
|
customInput={<DatePickerWrapper />}
|
||||||
|
withPortal={withPortal}
|
||||||
|
popperPlacement={popperPlacement}
|
||||||
|
popperModifiers={{
|
||||||
|
offset: {
|
||||||
|
enabled: true,
|
||||||
|
offset: "0px, 5px"
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: true,
|
||||||
|
escapeWithReference: false,
|
||||||
|
boundariesElement: "viewport"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,18 +1,19 @@
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import Hashtag from '../../../components/hashtag';
|
||||||
import AccountContainer from '../../../../containers/account_container';
|
import Icon from 'gabsocial/components/icon';
|
||||||
import StatusContainer from '../../../../containers/status_container';
|
import WhoToFollowPanel from '../../ui/components/who_to_follow_panel';
|
||||||
import TrendingItem from '../../../../components/trending_item';
|
// import TrendsPanel from '../../ui/components/trends_panel';
|
||||||
import Icon from '../../../../components/icon';
|
import GroupListItem from 'gabsocial/components/group_list_item';
|
||||||
import { WhoToFollowPanel } from '../../../../components/panel';
|
|
||||||
|
|
||||||
import './search_results.scss';
|
export default
|
||||||
|
@injectIntl
|
||||||
export default class SearchResults extends ImmutablePureComponent {
|
class SearchResults extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
results: ImmutablePropTypes.map.isRequired,
|
results: ImmutablePropTypes.map.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
location: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -20,7 +21,7 @@ export default class SearchResults extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { results } = this.props;
|
const { results, location } = this.props;
|
||||||
const { isSmallScreen } = this.state;
|
const { isSmallScreen } = this.state;
|
||||||
|
|
||||||
if (results.isEmpty() && isSmallScreen) {
|
if (results.isEmpty() && isSmallScreen) {
|
||||||
|
@ -31,44 +32,46 @@ export default class SearchResults extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts, statuses, hashtags;
|
const pathname = location.pathname || '';
|
||||||
|
const showPeople = pathname === '/search/people';
|
||||||
|
const showHashtags = pathname === '/search/hashtags';
|
||||||
|
const showGroups = pathname === '/search/groups';
|
||||||
|
const isTop = !showPeople && !showHashtags && !showGroups;
|
||||||
|
|
||||||
|
let accounts, statuses, hashtags, groups;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
|
||||||
count += results.get('accounts').size;
|
if (results.get('accounts') && results.get('accounts').size > 0 && (isTop || showPeople)) {
|
||||||
|
const size = isTop ? Math.min(results.get('accounts').size, 5) : results.get('accounts').size;
|
||||||
|
count += size;
|
||||||
accounts = (
|
accounts = (
|
||||||
<div className='search-results__section'>
|
<div className='search-results__section'>
|
||||||
<h5>
|
<h5><Icon id='user' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
|
||||||
<Icon id='users' fixedWidth />
|
{results.get('accounts').slice(0, size).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||||
<FormattedMessage id='search_results.accounts' defaultMessage='People' />
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
|
||||||
count += results.get('statuses').size;
|
if (results.get('groups') && results.get('groups').size > 0 && (isTop || showGroups)) {
|
||||||
statuses = (
|
const size = isTop ? Math.min(results.get('groups').size, 5) : results.get('groups').size;
|
||||||
|
count += size;
|
||||||
|
groups = (
|
||||||
<div className='search-results__section'>
|
<div className='search-results__section'>
|
||||||
<h5>
|
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.groups' defaultMessage='Groups' /></h5>
|
||||||
<Icon id='quote-right' fixedWidth />
|
{results.get('groups').slice(0, size).map(group => <GroupListItem key={`search-${group.get('name')}`} group={group} />)}
|
||||||
<FormattedMessage id='search_results.statuses' defaultMessage='Gabs' />
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
if (results.get('hashtags') && results.get('hashtags').size > 0 && (isTop || showHashtags)) {
|
||||||
count += results.get('hashtags').size;
|
const size = isTop ? Math.min(results.get('hashtags').size, 5) : results.get('hashtags').size;
|
||||||
|
count += size;
|
||||||
hashtags = (
|
hashtags = (
|
||||||
<div className='search-results__section'>
|
<div className='search-results__section'>
|
||||||
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
||||||
|
{results.get('hashtags').slice(0, size).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||||
{results.get('hashtags').map(hashtag => <TrendingItem key={hashtag.get('name')} hashtag={hashtag} />)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -87,6 +90,7 @@ export default class SearchResults extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{accounts}
|
{accounts}
|
||||||
|
{groups}
|
||||||
{statuses}
|
{statuses}
|
||||||
{hashtags}
|
{hashtags}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,8 +10,7 @@ import './upload.scss';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||||
undo: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
||||||
focus: { id: 'upload_form.focus', defaultMessage: 'Crop' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -100,24 +99,10 @@ class Upload extends ImmutablePureComponent {
|
||||||
<div className='compose-form-upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
|
<div className='compose-form-upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
|
||||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||||
{({ scale }) => (
|
{({ scale }) => (
|
||||||
<div className='compose-form-upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||||
<div className={classNames('compose-form-upload__actions', { active })}>
|
<div className={classNames('compose-form__upload__actions', { active })}>
|
||||||
<IconButton
|
<button className='icon-button' title={intl.formatMessage(messages.delete)} onClick={this.handleUndoClick}><Icon id='times'/></button>
|
||||||
onClick={this.handleUndoClick}
|
{media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
|
||||||
icon='times'
|
|
||||||
title={intl.formatMessage(messages.undo)}
|
|
||||||
text={intl.formatMessage(messages.undo)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
media.get('type') === 'image' &&
|
|
||||||
<IconButton
|
|
||||||
onClick={this.handleFocalPointClick}
|
|
||||||
icon='crosshairs'
|
|
||||||
title={intl.formatMessage(messages.focus)}
|
|
||||||
text={intl.formatMessage(messages.focus)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classNames('compose-form-upload__description', { active })}>
|
<div className={classNames('compose-form-upload__description', { active })}>
|
||||||
|
|
|
@ -8,9 +8,11 @@ import {
|
||||||
changeComposeSpoilerText,
|
changeComposeSpoilerText,
|
||||||
insertEmojiCompose,
|
insertEmojiCompose,
|
||||||
uploadCompose,
|
uploadCompose,
|
||||||
|
changeScheduledAt,
|
||||||
} from '../../../actions/compose';
|
} from '../../../actions/compose';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
edit: state.getIn(['compose', 'id']) !== null,
|
||||||
text: state.getIn(['compose', 'text']),
|
text: state.getIn(['compose', 'text']),
|
||||||
suggestions: state.getIn(['compose', 'suggestions']),
|
suggestions: state.getIn(['compose', 'suggestions']),
|
||||||
spoiler: state.getIn(['compose', 'spoiler']),
|
spoiler: state.getIn(['compose', 'spoiler']),
|
||||||
|
@ -25,6 +27,8 @@ const mapStateToProps = state => ({
|
||||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
isModalOpen: state.get('modal').modalType === 'COMPOSE',
|
isModalOpen: state.get('modal').modalType === 'COMPOSE',
|
||||||
|
quoteOfId: state.getIn(['compose', 'quote_of_id']),
|
||||||
|
scheduledAt: state.getIn(['compose', 'scheduled_at']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
@ -61,6 +65,9 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
dispatch(insertEmojiCompose(position, data, needsSpace));
|
dispatch(insertEmojiCompose(position, data, needsSpace));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setScheduledAt (date) {
|
||||||
|
dispatch(changeScheduledAt(date));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function mergeProps(stateProps, dispatchProps, ownProps) {
|
function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import QuotedStatusPreview from '../components/quoted_status_preview';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
status: state.getIn(['statuses', id]),
|
||||||
|
account: state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(QuotedStatusPreview);
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import SchedulePostDropdown from '../components/schedule_post_dropdown';
|
||||||
|
import { changeScheduledAt } from '../../../actions/compose';
|
||||||
|
import { openModal } from '../../../actions/modal';
|
||||||
|
import { me } from '../../../initial_state';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
date: state.getIn(['compose', 'scheduled_at']),
|
||||||
|
isPro: state.getIn(['accounts', me, 'is_pro']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
setScheduledAt (date) {
|
||||||
|
dispatch(changeScheduledAt(date));
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpenProUpgradeModal() {
|
||||||
|
dispatch(openModal('PRO_UPGRADE'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(SchedulePostDropdown);
|
|
@ -1,5 +1,6 @@
|
||||||
import SearchResults from '../components/search_results';
|
import SearchResults from '../components/search_results';
|
||||||
import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
|
import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
results: state.getIn(['search', 'results']),
|
results: state.getIn(['search', 'results']),
|
||||||
|
@ -11,4 +12,4 @@ const mapDispatchToProps = dispatch => ({
|
||||||
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
|
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SearchResults));
|
||||||
|
|
|
@ -6,17 +6,20 @@ import { Link } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import GroupCard from './card';
|
import GroupCard from './card';
|
||||||
import GroupCreate from '../create';
|
import GroupCreate from '../create';
|
||||||
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
import { openModal } from '../../../actions/modal';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
||||||
create: { id: 'groups.create', defaultMessage: 'Create group' },
|
create: { id: 'groups.create', defaultMessage: 'Create group' },
|
||||||
tab_featured: { id: 'groups.tab_featured', defaultMessage: 'Featured' },
|
tab_featured: { id: 'groups.tab_featured', defaultMessage: 'Featured' },
|
||||||
tab_member: { id: 'groups.tab_member', defaultMessage: 'Groups you\'re in' },
|
tab_member: { id: 'groups.tab_member', defaultMessage: 'Member' },
|
||||||
tab_admin: { id: 'groups.tab_admin', defaultMessage: 'Groups you manage' },
|
tab_admin: { id: 'groups.tab_admin', defaultMessage: 'Manage' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { activeTab }) => ({
|
const mapStateToProps = (state, { activeTab }) => ({
|
||||||
groupIds: state.getIn(['group_lists', activeTab]),
|
groupIds: state.getIn(['group_lists', activeTab]),
|
||||||
|
account: state.getIn(['accounts', me]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -42,12 +45,27 @@ class Groups extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleOpenProUpgradeModal = () => {
|
||||||
|
this.props.dispatch(openModal('PRO_UPGRADE'));
|
||||||
|
}
|
||||||
|
|
||||||
renderHeader() {
|
renderHeader() {
|
||||||
const { intl, activeTab } = this.props;
|
const { intl, activeTab, account, onOpenProUpgradeModal } = this.props;
|
||||||
|
|
||||||
|
const isPro = account.get('is_pro');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group-column-header">
|
<div className="group-column-header">
|
||||||
<div className="group-column-header__cta"><Link to="/groups/create" className="button">{intl.formatMessage(messages.create)}</Link></div>
|
<div className="group-column-header__cta">
|
||||||
|
{
|
||||||
|
account && isPro &&
|
||||||
|
<Link to="/groups/create" className="button standard-small">{intl.formatMessage(messages.create)}</Link>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
account && !isPro &&
|
||||||
|
<button onClick={this.handleOpenProUpgradeModal} className="button standard-small">{intl.formatMessage(messages.create)}</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div className="group-column-header__title">{intl.formatMessage(messages.heading)}</div>
|
<div className="group-column-header__title">{intl.formatMessage(messages.heading)}</div>
|
||||||
|
|
||||||
<div className="column-header__wrapper">
|
<div className="column-header__wrapper">
|
||||||
|
|
|
@ -3,18 +3,22 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import ColumnIndicator from '../../../components/column_indicator';
|
import ColumnIndicator from '../../../components/column_indicator';
|
||||||
import {
|
import {
|
||||||
fetchMembers,
|
fetchMembers,
|
||||||
expandMembers,
|
expandMembers,
|
||||||
|
updateRole,
|
||||||
|
createRemovedAccount,
|
||||||
} from '../../../actions/groups';
|
} from '../../../actions/groups';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import AccountContainer from '../../../containers/account_container';
|
import AccountContainer from '../../../containers/account_container';
|
||||||
import Column from '../../../components/column';
|
import Column from '../../../components/column';
|
||||||
import ScrollableList from '../../../components/scrollable_list';
|
import ScrollableList from '../../../components/scrollable_list';
|
||||||
|
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { id } }) => ({
|
const mapStateToProps = (state, { params: { id } }) => ({
|
||||||
group: state.getIn(['groups', id]),
|
group: state.getIn(['groups', id]),
|
||||||
accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
|
relationships: state.getIn(['group_relationships', id]),
|
||||||
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
|
accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -44,24 +48,44 @@ class GroupMembers extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountIds, hasMore, group } = this.props;
|
const { accountIds, hasMore, group, relationships, dispatch } = this.props;
|
||||||
|
|
||||||
|
if (!group || !accountIds || !relationships) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!group || !accountIds) {
|
return (
|
||||||
return (<ColumnIndicator type='loading' />);
|
<Column>
|
||||||
}
|
<ScrollableList
|
||||||
|
scrollKey='members'
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
emptyMessage={<FormattedMessage id='group.members.empty' defaultMessage='This group does not has any members.' />}
|
||||||
|
>
|
||||||
|
{accountIds.map(id => {
|
||||||
|
let menu = [];
|
||||||
|
|
||||||
return (
|
if (relationships.get('admin')) {
|
||||||
<Column>
|
menu = [
|
||||||
<ScrollableList
|
{ text: 'Remove from group', action: () => dispatch(createRemovedAccount(group.get('id'), id)) },
|
||||||
scrollKey='members'
|
{ text: 'Make administrator', action: () => dispatch(updateRole(group.get('id'), id, 'admin')) },
|
||||||
hasMore={hasMore}
|
]
|
||||||
onLoadMore={this.handleLoadMore}
|
}
|
||||||
emptyMessage={<FormattedMessage id='group.members.empty' defaultMessage='This group does not has any members.' />}
|
|
||||||
>
|
return (
|
||||||
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
|
<div className="group-account-wrapper" key={id}>
|
||||||
</ScrollableList>
|
<AccountContainer id={id} withNote={false} actionIcon="none" onActionClick={() => true} />
|
||||||
</Column>
|
{menu.length > 0 && <DropdownMenuContainer items={menu} icon='ellipsis-h' size={18} direction='right' />}
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollableList>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import SettingToggle from '../../../notifications/components/setting_toggle';
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { settings, onChange } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ColumnSettings from '../components/column_settings';
|
||||||
|
import { changeSetting, saveSettings } from '../../../../actions/settings';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
settings: state.getIn(['settings', 'group']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onChange (key, checked) {
|
||||||
|
dispatch(changeSetting(['group', ...key], checked));
|
||||||
|
},
|
||||||
|
|
||||||
|
onSave () {
|
||||||
|
dispatch(saveSettings());
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
|
@ -1,11 +1,24 @@
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||||
import StatusListContainer from '../../../containers/status_list_container';
|
import Column from '../../../components/column';
|
||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||||
import { connectGroupStream } from '../../../actions/streaming';
|
import { connectGroupStream } from '../../../actions/streaming';
|
||||||
import { expandGroupTimeline } from '../../../actions/timelines';
|
import { expandGroupTimeline } from '../../../actions/timelines';
|
||||||
import ColumnIndicator from '../../../components/column_indicator';
|
import MissingIndicator from '../../../components/missing_indicator';
|
||||||
import TimelineComposeBlock from '../../../components/timeline_compose_block';
|
import LoadingIndicator from '../../../components/loading_indicator';
|
||||||
|
import ComposeFormContainer from '../../../../gabsocial/features/compose/containers/compose_form_container';
|
||||||
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
import Avatar from '../../../components/avatar';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import ColumnSettingsContainer from "./containers/column_settings_container";
|
||||||
|
import Icon from 'gabsocial/components/icon';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
tabLatest: { id: 'group.timeline.tab_latest', defaultMessage: 'Latest' },
|
||||||
|
show: { id: 'group.timeline.show_settings', defaultMessage: 'Show settings' },
|
||||||
|
hide: { id: 'group.timeline.hide_settings', defaultMessage: 'Hide settings' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
group: state.getIn(['groups', props.params.id]),
|
group: state.getIn(['groups', props.params.id]),
|
||||||
|
@ -31,6 +44,10 @@ class GroupTimeline extends ImmutablePureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
collapsed: true,
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
|
@ -52,9 +69,15 @@ class GroupTimeline extends ImmutablePureComponent {
|
||||||
this.props.dispatch(expandGroupTimeline(id, { maxId }));
|
this.props.dispatch(expandGroupTimeline(id, { maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleToggleClick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({ collapsed: !this.state.collapsed });
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { columnId, group, relationships } = this.props;
|
const { columnId, group, relationships, account, intl } = this.props;
|
||||||
const { id } = this.props.params;
|
const { collapsed } = this.state;
|
||||||
|
const { id } = this.props.params;
|
||||||
|
|
||||||
if (typeof group === 'undefined' || !relationships) {
|
if (typeof group === 'undefined' || !relationships) {
|
||||||
return (<ColumnIndicator type='loading' />);
|
return (<ColumnIndicator type='loading' />);
|
||||||
|
@ -69,18 +92,44 @@ class GroupTimeline extends ImmutablePureComponent {
|
||||||
<TimelineComposeBlock size={46} group={group} shouldCondense={true} autoFocus={false} />
|
<TimelineComposeBlock size={46} group={group} shouldCondense={true} autoFocus={false} />
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className='group__feed'>
|
<div className='group__feed'>
|
||||||
<StatusListContainer
|
<div className="column-header__wrapper">
|
||||||
scrollKey={`group_timeline-${columnId}`}
|
<h1 className="column-header">
|
||||||
timelineId={`group:${id}`}
|
<Link to={`/groups/${id}`} className={classNames('btn grouped active')}>
|
||||||
onLoadMore={this.handleLoadMore}
|
{intl.formatMessage(messages.tabLatest)}
|
||||||
group={group}
|
</Link>
|
||||||
withGroupAdmin={relationships && relationships.get('admin')}
|
|
||||||
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
<div className='column-header__buttons'>
|
||||||
/>
|
<button
|
||||||
</div>
|
className={classNames('column-header__button', { 'active': !collapsed })}
|
||||||
</div>
|
title={intl.formatMessage(collapsed ? messages.show : messages.hide)}
|
||||||
);
|
aria-label={intl.formatMessage(collapsed ? messages.show : messages.hide)}
|
||||||
|
aria-pressed={collapsed ? 'false' : 'true'}
|
||||||
|
onClick={this.handleToggleClick}
|
||||||
|
><Icon id='sliders' /></button>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
{!collapsed && <div className='column-header__collapsible'>
|
||||||
|
<div className='column-header__collapsible-inner'>
|
||||||
|
<div className='column-header__collapsible__extra'>
|
||||||
|
<ColumnSettingsContainer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
alwaysPrepend
|
||||||
|
scrollKey={`group_timeline-${columnId}`}
|
||||||
|
timelineId={`group:${id}`}
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
group={group}
|
||||||
|
withGroupAdmin={relationships && relationships.get('admin')}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,16 +59,12 @@ export default class Card extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
card: ImmutablePropTypes.map,
|
card: ImmutablePropTypes.map,
|
||||||
maxDescription: PropTypes.number,
|
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
compact: PropTypes.bool,
|
|
||||||
defaultWidth: PropTypes.number,
|
defaultWidth: PropTypes.number,
|
||||||
cacheWidth: PropTypes.func,
|
cacheWidth: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
maxDescription: 50,
|
|
||||||
compact: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -132,37 +128,52 @@ export default class Card extends ImmutablePureComponent {
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
className='status-card__image status-card-video'
|
className='status-card__image status-card-video'
|
||||||
dangerouslySetInnerHTML={content}
|
dangerouslySetInnerHTML={content}
|
||||||
style={{ height }}
|
style={{
|
||||||
|
height,
|
||||||
|
paddingBottom: 0,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { card, maxDescription, compact } = this.props;
|
const { card } = this.props;
|
||||||
const { width, embedded } = this.state;
|
const { width, embedded } = this.state;
|
||||||
|
|
||||||
if (card === null) {
|
if (card === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
|
const maxDescription = 150;
|
||||||
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
|
const cardImg = card.get('image');
|
||||||
|
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
|
||||||
|
const horizontal = (card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
|
||||||
const interactive = card.get('type') !== 'link';
|
const interactive = card.get('type') !== 'link';
|
||||||
const className = classnames('status-card', { horizontal, compact, interactive });
|
const className = classnames('status-card', {
|
||||||
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
horizontal,
|
||||||
const ratio = card.get('width') / card.get('height');
|
interactive,
|
||||||
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
compact: !cardImg && !interactive,
|
||||||
|
});
|
||||||
|
const title = interactive ?
|
||||||
|
<a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'>
|
||||||
|
<strong>{card.get('title')}</strong>
|
||||||
|
</a>
|
||||||
|
: <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
||||||
|
|
||||||
const description = (
|
const description = (
|
||||||
<div className='status-card__content'>
|
<div className='status-card__content'>
|
||||||
{title}
|
{title}
|
||||||
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
|
{!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
|
||||||
<span className='status-card__host'>{provider}</span>
|
<span className='status-card__host'>
|
||||||
|
<Icon id='link' fixedWidth />
|
||||||
|
{' '}
|
||||||
|
{provider}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
let embed = '';
|
let embed = '';
|
||||||
let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />;
|
let thumbnail = card ? <div style={{ backgroundImage: `url(${cardImg})` }} className='status-card__image-image' /> : thumbnail = <div className='status-card__image-image' />;
|
||||||
|
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
if (embedded) {
|
if (embedded) {
|
||||||
|
@ -177,7 +188,6 @@ export default class Card extends ImmutablePureComponent {
|
||||||
embed = (
|
embed = (
|
||||||
<div className='status-card__image'>
|
<div className='status-card__image'>
|
||||||
{thumbnail}
|
{thumbnail}
|
||||||
|
|
||||||
<div className='status-card__actions'>
|
<div className='status-card__actions'>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
|
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
|
||||||
|
@ -191,10 +201,10 @@ export default class Card extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className={className} ref={this.setRef}>
|
<div className={className} ref={this.setRef}>
|
||||||
{embed}
|
{embed}
|
||||||
{!compact && description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (card.get('image')) {
|
} else if (cardImg) {
|
||||||
embed = (
|
embed = (
|
||||||
<div className='status-card__image'>
|
<div className='status-card__image'>
|
||||||
{thumbnail}
|
{thumbnail}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import StatusQuote from '../../../components/status_quote';
|
||||||
import { Link, NavLink } from 'react-router-dom';
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
import { FormattedDate, FormattedNumber } from 'react-intl';
|
import { FormattedDate, FormattedNumber } from 'react-intl';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
@ -32,12 +33,17 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
compact: PropTypes.bool,
|
compact: PropTypes.bool,
|
||||||
showMedia: PropTypes.bool,
|
showMedia: PropTypes.bool,
|
||||||
onToggleMediaVisibility: PropTypes.func,
|
onToggleMediaVisibility: PropTypes.func,
|
||||||
|
onShowRevisions: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
height: null,
|
height: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleShowRevisions = () => {
|
||||||
|
this.props.onShowRevisions(this.props.status);
|
||||||
|
}
|
||||||
|
|
||||||
handleOpenVideo = (media, startTime) => {
|
handleOpenVideo = (media, startTime) => {
|
||||||
this.props.onOpenVideo(media, startTime);
|
this.props.onOpenVideo(media, startTime);
|
||||||
}
|
}
|
||||||
|
@ -110,6 +116,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
blurhash={video.get('blurhash')}
|
blurhash={video.get('blurhash')}
|
||||||
src={video.get('url')}
|
src={video.get('url')}
|
||||||
alt={video.get('description')}
|
alt={video.get('description')}
|
||||||
|
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||||
width={300}
|
width={300}
|
||||||
height={150}
|
height={150}
|
||||||
inline
|
inline
|
||||||
|
@ -185,16 +192,21 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
{status.get('group') && (
|
{(status.get('group') || status.get('revised_at') !== null) && (
|
||||||
<div className='status__meta'>
|
<div className='status__meta'>
|
||||||
Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink>
|
{status.get('group') && <React.Fragment>Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink></React.Fragment>}
|
||||||
</div>
|
{status.get('revised_at') !== null && <a onClick={this.handleShowRevisions}> Edited</a>}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
|
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
|
{status.get('quote') && <StatusQuote
|
||||||
|
id={status.get('quote')}
|
||||||
|
/>}
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
|
|
|
@ -10,13 +10,16 @@ import './detailed_status_action_bar.scss';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||||
|
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
||||||
|
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
|
||||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
||||||
|
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
||||||
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
|
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
|
||||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||||
|
@ -50,6 +53,7 @@ class ActionBar extends ImmutablePureComponent {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
onReply: PropTypes.func.isRequired,
|
onReply: PropTypes.func.isRequired,
|
||||||
onReblog: PropTypes.func.isRequired,
|
onReblog: PropTypes.func.isRequired,
|
||||||
|
onQuote: PropTypes.func.isRequired,
|
||||||
onFavourite: PropTypes.func.isRequired,
|
onFavourite: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
|
@ -79,6 +83,14 @@ class ActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleQuoteClick = (e) => {
|
||||||
|
if (me) {
|
||||||
|
this.props.onQuote(this.props.status, e);
|
||||||
|
} else {
|
||||||
|
this.props.onOpenUnauthorizedModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleFavouriteClick = () => {
|
handleFavouriteClick = () => {
|
||||||
if (me) {
|
if (me) {
|
||||||
this.props.onFavourite(this.props.status);
|
this.props.onFavourite(this.props.status);
|
||||||
|
@ -91,8 +103,8 @@ class ActionBar extends ImmutablePureComponent {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history);
|
this.props.onDelete(this.props.status, this.context.router.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRedraftClick = () => {
|
handleEditClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onEdit(this.props.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMentionClick = () => {
|
handleMentionClick = () => {
|
||||||
|
@ -176,7 +188,7 @@ class ActionBar extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
} else {
|
} else {
|
||||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
@ -208,17 +220,11 @@ class ActionBar extends ImmutablePureComponent {
|
||||||
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
|
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='detailed-status-action-bar'>
|
<div className='detailed-status__action-bar'>
|
||||||
<div className='detailed-status-action-bar__button'>
|
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
|
||||||
<IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} />
|
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||||
</div>
|
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} title={reblog_disabled ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-left' onClick={this.handleQuoteClick} /></div>
|
||||||
<div className='detailed-status-action-bar__button'>
|
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||||
<IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
|
||||||
</div>
|
|
||||||
<div className='detailed-status-action-bar__button'>
|
|
||||||
<IconButton className='star-icon' active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
<div className='detailed-status-action-bar__dropdown'>
|
<div className='detailed-status-action-bar__dropdown'>
|
||||||
|
|
|
@ -31,8 +31,6 @@ import { showAlertForError } from '../../../actions/alerts';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
|
||||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' },
|
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
|
@ -106,14 +104,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
onDelete (status, history, withRedraft = false) {
|
onDelete (status, history) {
|
||||||
if (!deleteModal) {
|
if (!deleteModal) {
|
||||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
dispatch(deleteStatus(status.get('id'), history));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
message: intl.formatMessage(messages.deleteMessage),
|
||||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
onConfirm: () => dispatch(deleteStatus(status.get('id'), history)),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||||
|
import { me } from '../../../initial_state';
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
<NavLink key='pr1' className='footer-bar__link' to='/home' data-preview-title-id='column.home'>
|
||||||
|
<i className='tabs-bar__link__icon home'/>
|
||||||
|
<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />
|
||||||
|
</NavLink>,
|
||||||
|
<NavLink key='pr2' className='footer-bar__link' to='/notifications' data-preview-title-id='column.notifications'>
|
||||||
|
<i className='tabs-bar__link__icon notifications'/>
|
||||||
|
<NotificationsCounterIcon />
|
||||||
|
<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />
|
||||||
|
</NavLink>,
|
||||||
|
<NavLink key='pr3' className='footer-bar__link' to='/groups' data-preview-title-id='column.groups'>
|
||||||
|
<i className='tabs-bar__link__icon groups'/>
|
||||||
|
<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />
|
||||||
|
</NavLink>,
|
||||||
|
<a key='pl4' className='footer-bar__link footer-bar__link--trends' href='https://trends.gab.com' data-preview-title-id='tabs_bar.trends'>
|
||||||
|
<i className='tabs-bar__link__icon trends'/>
|
||||||
|
<FormattedMessage id='tabs_bar.trends' defaultMessage='Trends' />
|
||||||
|
</a>,
|
||||||
|
]
|
||||||
|
|
||||||
|
export default
|
||||||
|
@injectIntl
|
||||||
|
@withRouter
|
||||||
|
class FooterBar extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl: { formatMessage } } = this.props;
|
||||||
|
|
||||||
|
if (!me) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='footer-bar'>
|
||||||
|
<div className='footer-bar__container'>
|
||||||
|
{
|
||||||
|
links.map((link) =>
|
||||||
|
React.cloneElement(link, {
|
||||||
|
key: link.props.to,
|
||||||
|
'aria-label': formatMessage({
|
||||||
|
id: link.props['data-preview-title-id']
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { me } from '../../../initial_state';
|
||||||
|
import IconButton from '../../../components/icon_button';
|
||||||
|
import Icon from '../../../components/icon';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
account: state.getIn(['accounts', me]),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProUpgradeModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickClose = () => {
|
||||||
|
this.props.onClose('PRO_UPGRADE');
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal compose-modal pro-upgrade-modal'>
|
||||||
|
<div className='compose-modal__header'>
|
||||||
|
<h3 className='compose-modal__header__title'><FormattedMessage id='promo.gab_pro' defaultMessage='Upgrade to GabPRO' /></h3>
|
||||||
|
<IconButton className='compose-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={this.onClickClose} size={20} />
|
||||||
|
</div>
|
||||||
|
<div className='compose-modal__content pro-upgrade-modal__content'>
|
||||||
|
<div>
|
||||||
|
<span className="pro-upgrade-modal__text">
|
||||||
|
<FormattedMessage id='pro_upgrade_modal.text' defaultMessage='Gab is fully funded by people like you. Please consider supporting us on our mission to defend free expression online for all people.' />
|
||||||
|
<FormattedMessage id='pro_upgrade_modal.benefits' defaultMessage='Here are just some of the benefits that thousands of GabPRO members receive:' />
|
||||||
|
</span>
|
||||||
|
<ul className="pro-upgrade-modal__list">
|
||||||
|
<li>Schedule Posts</li>
|
||||||
|
<li>Get Verified</li>
|
||||||
|
<li>Create Groups</li>
|
||||||
|
<li>Larger Video and Image Uploads</li>
|
||||||
|
<li>Receive the PRO Badge</li>
|
||||||
|
<li>Remove in-feed promotions</li>
|
||||||
|
<li>More value being added daily!</li>
|
||||||
|
</ul>
|
||||||
|
<a href='https://pro.gab.com' className='pro-upgrade-modal__button button'>
|
||||||
|
<Icon id='arrow-up' fixedWidth/>
|
||||||
|
<FormattedMessage id='promo.gab_pro' defaultMessage='Upgrade to GabPRO' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(connect(mapStateToProps)(ProUpgradeModal));
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { monthlyExpensesComplete } from '../../../initial_state';
|
||||||
|
|
||||||
|
export default class ProgressPanel extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
if (!monthlyExpensesComplete) return null;
|
||||||
|
|
||||||
|
const completed = Math.min(monthlyExpensesComplete, 100);
|
||||||
|
const style = {
|
||||||
|
width: `${completed}%`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='wtf-panel progress-panel'>
|
||||||
|
<div className='wtf-panel-header progress-panel-header'>
|
||||||
|
<div className='wtf-panel-header__label'>Gab's Operational Expenses</div>
|
||||||
|
</div>
|
||||||
|
<div className='wtf-panel__content progress-panel__content'>
|
||||||
|
<span className='progress-panel__text'>We are 100% funded by you.</span>
|
||||||
|
<div className='progress-panel__bar-container'>
|
||||||
|
<a className='progress-panel__bar' style={style} href='https://shop.dissenter.com/category/donations'>
|
||||||
|
<span className='progress-panel__bar__text'>{completed}% covered this month</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { injectIntl } from 'react-intl';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ModalLoading from './modal_loading';
|
||||||
|
import RelativeTimestamp from '../../../components/relative_timestamp';
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class StatusRevisionsList extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
data: PropTypes.array
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { loading, error, data } = this.props;
|
||||||
|
|
||||||
|
if (loading || !data) return <ModalLoading />;
|
||||||
|
|
||||||
|
if (error) return (
|
||||||
|
<div className='status-revisions-list'>
|
||||||
|
<div className='status-revisions-list__error'>
|
||||||
|
An error occured
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='status-revisions-list'>
|
||||||
|
{data.map((revision, i) => (
|
||||||
|
<div key={i} className='status-revisions-list__item'>
|
||||||
|
<div className='status-revisions-list__item__timestamp'>
|
||||||
|
<RelativeTimestamp timestamp={revision.created_at} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='status-revisions-list__item__text'>{revision.text}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import IconButton from 'gabsocial/components/icon_button';
|
||||||
|
import StatusRevisionListContainer from '../containers/status_revision_list_container';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class StatusRevisionModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
status: ImmutablePropTypes.map.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, onClose, status } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal status-revisions-root'>
|
||||||
|
<div className='status-revisions'>
|
||||||
|
<div className='status-revisions__header'>
|
||||||
|
<h3 className='status-revisions__header__title'><FormattedMessage id='status_revisions.heading' defaultMessage='Revision History' /></h3>
|
||||||
|
<IconButton className='status-revisions__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='status-revisions__content'>
|
||||||
|
<StatusRevisionListContainer id={status.get('id')} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { load } from '../../../actions/status_revision_list';
|
||||||
|
import StatusRevisionList from '../components/status_revision_list';
|
||||||
|
|
||||||
|
class StatusRevisionListContainer extends ImmutablePureComponent {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.load(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <StatusRevisionList {...this.props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
loading: state.getIn(['status_revision_list', 'loading']),
|
||||||
|
error: state.getIn(['status_revision_list', 'error']),
|
||||||
|
data: state.getIn(['status_revision_list', 'data']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
load
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(StatusRevisionListContainer);
|
|
@ -110,6 +110,10 @@ export function MuteModal () {
|
||||||
return import(/* webpackChunkName: "modals/mute_modal" */'../../../components/modal');
|
return import(/* webpackChunkName: "modals/mute_modal" */'../../../components/modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function StatusRevisionModal () {
|
||||||
|
return import(/* webpackChunkName: "modals/mute_modal" */'../components/status_revision_modal');
|
||||||
|
}
|
||||||
|
|
||||||
export function ReportModal () {
|
export function ReportModal () {
|
||||||
return import(/* webpackChunkName: "modals/report_modal" */'../../../components/modal');
|
return import(/* webpackChunkName: "modals/report_modal" */'../../../components/modal');
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,5 +23,8 @@ export const mascot = getMeta('mascot');
|
||||||
export const profile_directory = getMeta('profile_directory');
|
export const profile_directory = getMeta('profile_directory');
|
||||||
export const isStaff = getMeta('is_staff');
|
export const isStaff = getMeta('is_staff');
|
||||||
export const forceSingleColumn = !getMeta('advanced_layout');
|
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||||
|
export const promotions = initialState && initialState.promotions;
|
||||||
|
export const unreadCount = getMeta('unread_count');
|
||||||
|
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "etiqueta",
|
"search_popout.tips.hashtag": "etiqueta",
|
||||||
"search_popout.tips.status": "estáu",
|
"search_popout.tips.status": "estáu",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "usuariu",
|
"search_popout.tips.user": "usuariu",
|
||||||
"search_results.accounts": "Xente",
|
"search_results.accounts": "Xente",
|
||||||
"search_results.hashtags": "Etiquetes",
|
"search_results.hashtags": "Etiquetes",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -1026,7 +1026,7 @@
|
||||||
"id": "search_popout.tips.full_text"
|
"id": "search_popout.tips.full_text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Simple text returns matching display names, usernames and hashtags",
|
"defaultMessage": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"id": "search_popout.tips.text"
|
"id": "search_popout.tips.text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -310,7 +310,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favorited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favorited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "felhasználó",
|
"search_popout.tips.user": "felhasználó",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "tagar",
|
"search_popout.tips.hashtag": "tagar",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "แฮชแท็ก",
|
"search_popout.tips.hashtag": "แฮชแท็ก",
|
||||||
"search_popout.tips.status": "สถานะ",
|
"search_popout.tips.status": "สถานะ",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "ผู้ใช้",
|
"search_popout.tips.user": "ผู้ใช้",
|
||||||
"search_results.accounts": "ผู้คน",
|
"search_results.accounts": "ผู้คน",
|
||||||
"search_results.hashtags": "แฮชแท็ก",
|
"search_results.hashtags": "แฮชแท็ก",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "durum",
|
"search_popout.tips.status": "durum",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "kullanıcı",
|
"search_popout.tips.user": "kullanıcı",
|
||||||
"search_results.accounts": "İnsanlar",
|
"search_results.accounts": "İnsanlar",
|
||||||
"search_results.hashtags": "Hashtagler",
|
"search_results.hashtags": "Hashtagler",
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "status",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames, groups and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
"search_results.hashtags": "Hashtags",
|
"search_results.hashtags": "Hashtags",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
COMPOSE_CHANGE,
|
COMPOSE_CHANGE,
|
||||||
COMPOSE_REPLY,
|
COMPOSE_REPLY,
|
||||||
COMPOSE_REPLY_CANCEL,
|
COMPOSE_REPLY_CANCEL,
|
||||||
|
COMPOSE_QUOTE,
|
||||||
COMPOSE_DIRECT,
|
COMPOSE_DIRECT,
|
||||||
COMPOSE_MENTION,
|
COMPOSE_MENTION,
|
||||||
COMPOSE_SUBMIT_REQUEST,
|
COMPOSE_SUBMIT_REQUEST,
|
||||||
|
@ -35,16 +36,18 @@ import {
|
||||||
COMPOSE_POLL_OPTION_CHANGE,
|
COMPOSE_POLL_OPTION_CHANGE,
|
||||||
COMPOSE_POLL_OPTION_REMOVE,
|
COMPOSE_POLL_OPTION_REMOVE,
|
||||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||||
|
COMPOSE_SCHEDULED_AT_CHANGE,
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
import { STORE_HYDRATE } from '../actions/store';
|
import { STORE_HYDRATE } from '../actions/store';
|
||||||
import { REDRAFT } from '../actions/statuses';
|
import { STATUS_EDIT } from '../actions/statuses';
|
||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
import uuid from '../utils/uuid';
|
import uuid from '../utils/uuid';
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
import { unescapeHTML } from '../utils/html';
|
import { unescapeHTML } from '../utils/html';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
|
id: null,
|
||||||
mounted: 0,
|
mounted: 0,
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
spoiler: false,
|
spoiler: false,
|
||||||
|
@ -55,6 +58,7 @@ const initialState = ImmutableMap({
|
||||||
caretPosition: null,
|
caretPosition: null,
|
||||||
preselectDate: null,
|
preselectDate: null,
|
||||||
in_reply_to: null,
|
in_reply_to: null,
|
||||||
|
quote_of_id: null,
|
||||||
is_composing: false,
|
is_composing: false,
|
||||||
is_submitting: false,
|
is_submitting: false,
|
||||||
is_changing_upload: false,
|
is_changing_upload: false,
|
||||||
|
@ -69,6 +73,7 @@ const initialState = ImmutableMap({
|
||||||
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
||||||
idempotencyKey: null,
|
idempotencyKey: null,
|
||||||
tagHistory: ImmutableList(),
|
tagHistory: ImmutableList(),
|
||||||
|
scheduled_at: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialPoll = ImmutableMap({
|
const initialPoll = ImmutableMap({
|
||||||
|
@ -89,17 +94,20 @@ function statusToTextMentions(state, status) {
|
||||||
|
|
||||||
function clearAll(state) {
|
function clearAll(state) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
|
map.set('id', null);
|
||||||
map.set('text', '');
|
map.set('text', '');
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
map.set('is_submitting', false);
|
map.set('is_submitting', false);
|
||||||
map.set('is_changing_upload', false);
|
map.set('is_changing_upload', false);
|
||||||
map.set('in_reply_to', null);
|
map.set('in_reply_to', null);
|
||||||
|
map.set('quote_of_id', null);
|
||||||
map.set('privacy', state.get('default_privacy'));
|
map.set('privacy', state.get('default_privacy'));
|
||||||
map.set('sensitive', false);
|
map.set('sensitive', false);
|
||||||
map.update('media_attachments', list => list.clear());
|
map.update('media_attachments', list => list.clear());
|
||||||
map.set('poll', null);
|
map.set('poll', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
map.set('scheduled_at', null);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -189,7 +197,10 @@ const expandMentions = status => {
|
||||||
const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
|
const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
|
||||||
|
|
||||||
status.get('mentions').forEach(mention => {
|
status.get('mentions').forEach(mention => {
|
||||||
fragment.querySelector(`a[href="/${mention.get('acct')}"]`).textContent = `@${mention.get('acct')}`;
|
const mentionFragment = fragment.querySelector(`a[href$="/${mention.get('acct')}"]`);
|
||||||
|
if (mentionFragment) {
|
||||||
|
fragment.querySelector(`a[href$="/${mention.get('acct')}"]`).textContent = `@${mention.get('acct')}`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return fragment.innerHTML;
|
return fragment.innerHTML;
|
||||||
|
@ -247,6 +258,24 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('preselectDate', new Date());
|
map.set('preselectDate', new Date());
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
|
||||||
|
if (action.status.get('spoiler_text').length > 0) {
|
||||||
|
map.set('spoiler', true);
|
||||||
|
map.set('spoiler_text', action.status.get('spoiler_text'));
|
||||||
|
} else {
|
||||||
|
map.set('spoiler', false);
|
||||||
|
map.set('spoiler_text', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
case COMPOSE_QUOTE:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('quote_of_id', action.status.get('id'));
|
||||||
|
map.set('text', '');
|
||||||
|
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||||
|
map.set('focusDate', new Date());
|
||||||
|
map.set('caretPosition', null);
|
||||||
|
map.set('preselectDate', new Date());
|
||||||
|
map.set('idempotencyKey', uuid());
|
||||||
|
|
||||||
if (action.status.get('spoiler_text').length > 0) {
|
if (action.status.get('spoiler_text').length > 0) {
|
||||||
map.set('spoiler', true);
|
map.set('spoiler', true);
|
||||||
map.set('spoiler_text', action.status.get('spoiler_text'));
|
map.set('spoiler_text', action.status.get('spoiler_text'));
|
||||||
|
@ -258,6 +287,8 @@ export default function compose(state = initialState, action) {
|
||||||
case COMPOSE_REPLY_CANCEL:
|
case COMPOSE_REPLY_CANCEL:
|
||||||
case COMPOSE_RESET:
|
case COMPOSE_RESET:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
|
map.set('id', null);
|
||||||
|
map.set('quote_of_id', null);
|
||||||
map.set('in_reply_to', null);
|
map.set('in_reply_to', null);
|
||||||
map.set('text', '');
|
map.set('text', '');
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
|
@ -265,6 +296,7 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('privacy', state.get('default_privacy'));
|
map.set('privacy', state.get('default_privacy'));
|
||||||
map.set('poll', null);
|
map.set('poll', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
map.set('scheduled_at', null);
|
||||||
});
|
});
|
||||||
case COMPOSE_SUBMIT_REQUEST:
|
case COMPOSE_SUBMIT_REQUEST:
|
||||||
return state.set('is_submitting', true);
|
return state.set('is_submitting', true);
|
||||||
|
@ -329,10 +361,12 @@ export default function compose(state = initialState, action) {
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}));
|
}));
|
||||||
case REDRAFT:
|
case STATUS_EDIT:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
|
map.set('id', action.status.get('id'));
|
||||||
|
map.set('text', unescapeHTML(expandMentions(action.status)));
|
||||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||||
|
map.set('quote_of_id', action.status.get('quote_of_id'));
|
||||||
map.set('privacy', action.status.get('visibility'));
|
map.set('privacy', action.status.get('visibility'));
|
||||||
map.set('media_attachments', action.status.get('media_attachments'));
|
map.set('media_attachments', action.status.get('media_attachments'));
|
||||||
map.set('focusDate', new Date());
|
map.set('focusDate', new Date());
|
||||||
|
@ -346,14 +380,6 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.status.get('poll')) {
|
|
||||||
map.set('poll', ImmutableMap({
|
|
||||||
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
|
||||||
multiple: action.status.getIn(['poll', 'multiple']),
|
|
||||||
expires_in: 24 * 3600,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
case COMPOSE_POLL_ADD:
|
case COMPOSE_POLL_ADD:
|
||||||
return state.set('poll', initialPoll);
|
return state.set('poll', initialPoll);
|
||||||
|
@ -367,6 +393,8 @@ export default function compose(state = initialState, action) {
|
||||||
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
|
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
|
||||||
case COMPOSE_POLL_SETTINGS_CHANGE:
|
case COMPOSE_POLL_SETTINGS_CHANGE:
|
||||||
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||||
|
case COMPOSE_SCHEDULED_AT_CHANGE:
|
||||||
|
return state.set('scheduled_at', action.date);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,8 @@ import groups from './groups';
|
||||||
import group_relationships from './group_relationships';
|
import group_relationships from './group_relationships';
|
||||||
import group_lists from './group_lists';
|
import group_lists from './group_lists';
|
||||||
import group_editor from './group_editor';
|
import group_editor from './group_editor';
|
||||||
|
import sidebar from './sidebar';
|
||||||
|
import status_revision_list from './status_revision_list';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
dropdown_menu,
|
dropdown_menu,
|
||||||
|
@ -75,6 +77,8 @@ const reducers = {
|
||||||
group_relationships,
|
group_relationships,
|
||||||
group_lists,
|
group_lists,
|
||||||
group_editor,
|
group_editor,
|
||||||
|
sidebar,
|
||||||
|
status_revision_list,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
export default combineReducers(reducers);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
NOTIFICATIONS_INITIALIZE,
|
||||||
NOTIFICATIONS_UPDATE,
|
NOTIFICATIONS_UPDATE,
|
||||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
NOTIFICATIONS_EXPAND_SUCCESS,
|
||||||
NOTIFICATIONS_EXPAND_REQUEST,
|
NOTIFICATIONS_EXPAND_REQUEST,
|
||||||
|
@ -9,6 +10,7 @@ import {
|
||||||
NOTIFICATIONS_UPDATE_QUEUE,
|
NOTIFICATIONS_UPDATE_QUEUE,
|
||||||
NOTIFICATIONS_DEQUEUE,
|
NOTIFICATIONS_DEQUEUE,
|
||||||
MAX_QUEUED_NOTIFICATIONS,
|
MAX_QUEUED_NOTIFICATIONS,
|
||||||
|
NOTIFICATIONS_MARK_READ,
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
|
@ -17,6 +19,7 @@ import {
|
||||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import compareId from '../utils/compare_id';
|
import compareId from '../utils/compare_id';
|
||||||
|
import { unreadCount } from 'gabsocial/initial_state';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
|
@ -26,6 +29,7 @@ const initialState = ImmutableMap({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
|
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
|
||||||
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
|
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
|
||||||
|
lastRead: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationToMap = notification => ImmutableMap({
|
const notificationToMap = notification => ImmutableMap({
|
||||||
|
@ -126,6 +130,10 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
|
||||||
|
|
||||||
export default function notifications(state = initialState, action) {
|
export default function notifications(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case NOTIFICATIONS_INITIALIZE:
|
||||||
|
return state.set('unread', unreadCount);
|
||||||
|
case NOTIFICATIONS_MARK_READ:
|
||||||
|
return state.set('lastRead', action.notification);
|
||||||
case NOTIFICATIONS_EXPAND_REQUEST:
|
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||||
return state.set('isLoading', true);
|
return state.set('isLoading', true);
|
||||||
case NOTIFICATIONS_EXPAND_FAIL:
|
case NOTIFICATIONS_EXPAND_FAIL:
|
||||||
|
|
|
@ -43,6 +43,7 @@ export default function search(state = initialState, action) {
|
||||||
accounts: ImmutableList(action.results.accounts.map(item => item.id)),
|
accounts: ImmutableList(action.results.accounts.map(item => item.id)),
|
||||||
statuses: ImmutableList(action.results.statuses.map(item => item.id)),
|
statuses: ImmutableList(action.results.statuses.map(item => item.id)),
|
||||||
hashtags: fromJS(action.results.hashtags),
|
hashtags: fromJS(action.results.hashtags),
|
||||||
|
groups: fromJS(action.results.groups),
|
||||||
})).set('submitted', true);
|
})).set('submitted', true);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -24,6 +24,12 @@ const initialState = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
group: ImmutableMap({
|
||||||
|
shows: ImmutableMap({
|
||||||
|
reply: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
notifications: ImmutableMap({
|
notifications: ImmutableMap({
|
||||||
alerts: ImmutableMap({
|
alerts: ImmutableMap({
|
||||||
follow: true,
|
follow: true,
|
||||||
|
@ -57,6 +63,10 @@ const initialState = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
community: ImmutableMap({
|
community: ImmutableMap({
|
||||||
|
other: ImmutableMap({
|
||||||
|
allFediverse: false,
|
||||||
|
onlyMedia: false,
|
||||||
|
}),
|
||||||
regex: ImmutableMap({
|
regex: ImmutableMap({
|
||||||
body: '',
|
body: '',
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { SIDEBAR_OPEN, SIDEBAR_CLOSE } from '../actions/sidebar';
|
||||||
|
|
||||||
|
export default function sidebar(state={}, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case SIDEBAR_OPEN:
|
||||||
|
return { sidebarOpen: true };
|
||||||
|
case SIDEBAR_CLOSE:
|
||||||
|
return { sidebarOpen: false };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
import {
|
||||||
|
STATUS_REVISION_LIST_LOAD,
|
||||||
|
STATUS_REVISION_LIST_LOAD_SUCCESS,
|
||||||
|
STATUS_REVISION_LIST_LOAD_FAIL
|
||||||
|
} from '../actions/status_revision_list';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
data: null
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function statusRevisionList(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case STATUS_REVISION_LIST_LOAD:
|
||||||
|
return initialState;
|
||||||
|
case STATUS_REVISION_LIST_LOAD_SUCCESS:
|
||||||
|
return state.withMutations(mutable => {
|
||||||
|
mutable.set('loading', false);
|
||||||
|
mutable.set('data', action.payload);
|
||||||
|
});
|
||||||
|
case STATUS_REVISION_LIST_LOAD_FAIL:
|
||||||
|
return state.withMutations(mutable => {
|
||||||
|
mutable.set('loading', false);
|
||||||
|
mutable.set('error', action.payload);
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
export const minimumAspectRatio = .8;
|
||||||
|
export const maximumAspectRatio = 2.8;
|
||||||
|
|
||||||
|
export const isPanoramic = ar => {
|
||||||
|
if (isNaN(ar)) return false;
|
||||||
|
return ar >= maximumAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isPortrait = ar => {
|
||||||
|
if (isNaN(ar)) return false;
|
||||||
|
return ar <= minimumAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isNonConformingRatio = ar => {
|
||||||
|
if (isNaN(ar)) return false;
|
||||||
|
return !isPanoramic(ar) && !isPortrait(ar);
|
||||||
|
}
|
|
@ -25,8 +25,12 @@
|
||||||
@import 'gabsocial/components/compose-form';
|
@import 'gabsocial/components/compose-form';
|
||||||
@import 'gabsocial/components/group-card';
|
@import 'gabsocial/components/group-card';
|
||||||
@import 'gabsocial/components/group-detail';
|
@import 'gabsocial/components/group-detail';
|
||||||
|
@import 'gabsocial/components/group-accounts';
|
||||||
@import 'gabsocial/components/group-form';
|
@import 'gabsocial/components/group-form';
|
||||||
@import 'gabsocial/components/group-sidebar-panel';
|
@import 'gabsocial/components/group-sidebar-panel';
|
||||||
|
@import 'gabsocial/components/sidebar-menu';
|
||||||
|
@import 'gabsocial/components/status-revisions';
|
||||||
|
@import 'gabsocial/components/footer-bar';
|
||||||
|
|
||||||
@import 'gabsocial/polls';
|
@import 'gabsocial/polls';
|
||||||
@import 'gabsocial/emoji_picker';
|
@import 'gabsocial/emoji_picker';
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
.footer-bar {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
background: #000;
|
||||||
|
height: 58px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
|
||||||
|
@media screen and (min-width: 895px) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin: 0;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 58px;
|
||||||
|
padding-top: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-top: 2px solid transparent;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-top-color: $gab-brand-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
.group-account-wrapper {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& > div > .icon-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue