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
|
||||
|
||||
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. Statuses were renamed from 'toots' to 'gabs'
|
||||
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. 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
|
||||
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_PUB_KEY`: Public key that is used when creating an access token or pairing https://btcpay.xxx.com/stores/yyy/Tokens/Create
|
||||
- `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.[yourdomain].com/stores/[yourstore]/Tokens/Create
|
||||
- `BTCPAY_MERCHANT_TOKEN`: Token created for facade *merchant*
|
||||
|
||||
## Deployment
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Admin
|
||||
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_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||
|
||||
|
@ -173,6 +173,22 @@ module Admin
|
|||
redirect_to edit_pro_admin_account_path(@account.id)
|
||||
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
|
||||
|
||||
def set_account
|
||||
|
@ -211,5 +227,14 @@ module Admin
|
|||
def pro_params
|
||||
params.require(:account).permit(:is_pro, :pro_expires_at)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
def check_account_suspension
|
||||
|
|
|
@ -20,13 +20,19 @@ class Api::V1::Groups::AccountsController < Api::BaseController
|
|||
authorize @group, :join?
|
||||
|
||||
@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
|
||||
end
|
||||
|
||||
def update
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -34,6 +40,11 @@ class Api::V1::Groups::AccountsController < Api::BaseController
|
|||
authorize @group, :leave?
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ class Api::V1::GroupsController < Api::BaseController
|
|||
def index
|
||||
case current_tab
|
||||
when 'featured'
|
||||
@groups = Group.where(is_featured: true).limit(25).all
|
||||
@groups = Group.where(is_featured: true).limit(50).all
|
||||
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
|
||||
when 'admin'
|
||||
|
@ -33,6 +33,8 @@ class Api::V1::GroupsController < Api::BaseController
|
|||
end
|
||||
|
||||
def create
|
||||
authorize :group, :create?
|
||||
|
||||
@group = Group.create!(group_params.merge(account: current_account))
|
||||
render json: @group, serializer: REST::GroupSerializer
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::MediaController < Api::BaseController
|
|||
respond_to :json
|
||||
|
||||
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
|
||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
||||
render json: file_type_error, status: 422
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::NotificationsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss]
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss, :mark_read]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss, :mark_read]
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
|
@ -30,6 +30,11 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
render_empty
|
||||
end
|
||||
|
||||
def mark_read
|
||||
current_account.notifications.find(params[:id]).mark_read!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_notifications
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::SearchController < Api::BaseController
|
||||
RESULTS_LIMIT = 20
|
||||
RESULTS_LIMIT = 100
|
||||
|
||||
respond_to :json
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
class Api::V1::StatusesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||
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
|
||||
|
||||
|
@ -33,14 +33,10 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||
end
|
||||
|
||||
def card
|
||||
@card = @status.preview_cards.first
|
||||
def revisions
|
||||
@revisions = @status.revisions
|
||||
|
||||
if @card.nil?
|
||||
render_empty
|
||||
else
|
||||
render json: @card, serializer: REST::PreviewCardSerializer
|
||||
end
|
||||
render json: @revisions, each_serializer: REST::StatusRevisionSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -55,11 +51,27 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
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
|
||||
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
|
||||
@status = Status.where(account_id: current_user.account).find(params[:id])
|
||||
authorize @status, :destroy?
|
||||
|
@ -82,6 +94,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
params.permit(
|
||||
:status,
|
||||
:in_reply_to_id,
|
||||
:quote_of_id,
|
||||
:sensitive,
|
||||
:spoiler_text,
|
||||
:visibility,
|
||||
|
|
|
@ -38,7 +38,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController
|
|||
statuses = group_timeline_statuses.without_replies.paginate_by_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) }
|
||||
|
||||
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.
|
||||
|
|
|
@ -4,6 +4,7 @@ class HomeController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
before_action :set_referrer_policy_header
|
||||
before_action :set_initial_state_json
|
||||
before_action :set_data_for_meta
|
||||
|
||||
def index
|
||||
@body_classes = 'app-body'
|
||||
|
@ -11,17 +12,40 @@ class HomeController < ApplicationController
|
|||
|
||||
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!
|
||||
return if user_signed_in?
|
||||
|
||||
# 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 matches
|
||||
if find_route_matches
|
||||
redirect_to(homepage_path)
|
||||
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
|
||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
|
||||
@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
|
||||
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)
|
||||
)
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
|
||||
def update
|
||||
user_settings.update(user_settings_params.to_h)
|
||||
current_user.force_regeneration!
|
||||
|
||||
if current_user.update(user_params)
|
||||
I18n.locale = current_user.locale
|
||||
|
@ -51,7 +52,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_show_application,
|
||||
:setting_advanced_layout,
|
||||
: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)
|
||||
)
|
||||
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 { CancelToken, isCancel } from 'axios';
|
||||
import { throttle } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { search as emojiSearch } from '../components/emoji/emoji_mart_search_light';
|
||||
import { tagHistory } from '../settings';
|
||||
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_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
||||
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
||||
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
||||
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
||||
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
||||
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_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
||||
|
||||
export const COMPOSE_SCHEDULED_AT_CHANGE = 'COMPOSE_SCHEDULED_AT_CHANGE';
|
||||
|
||||
const messages = defineMessages({
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
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() {
|
||||
return {
|
||||
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) {
|
||||
return function (dispatch, getState) {
|
||||
if (!me) return;
|
||||
|
@ -139,9 +193,20 @@ export function submitCompose(routerHistory, group) {
|
|||
dispatch(submitComposeRequest());
|
||||
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,
|
||||
scheduled_at,
|
||||
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')),
|
||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
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) {
|
||||
routerHistory.push('/messages');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
handleComposeSubmit(dispatch, getState, response, status);
|
||||
}).catch(function (error) {
|
||||
dispatch(submitComposeFail(error));
|
||||
});
|
||||
|
@ -561,3 +600,10 @@ export function changePollSettings(expiresIn, 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_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) => {
|
||||
if (!me) return;
|
||||
|
||||
|
@ -521,4 +525,43 @@ export function groupRemoveStatusFail(groupId, id, error) {
|
|||
id,
|
||||
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);
|
||||
}
|
||||
|
||||
if (status.quote && status.quote.id) {
|
||||
processStatus(status.quote);
|
||||
}
|
||||
|
||||
if (status.poll && status.poll.id) {
|
||||
pushUnique(polls, normalizePoll(status.poll));
|
||||
}
|
||||
|
|
|
@ -43,13 +43,17 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||
normalStatus.reblog = status.reblog.id;
|
||||
}
|
||||
|
||||
if (status.quote && status.quote.id) {
|
||||
normalStatus.quote = status.quote.id;
|
||||
}
|
||||
|
||||
if (status.poll && status.poll.id) {
|
||||
normalStatus.poll = status.poll.id;
|
||||
}
|
||||
|
||||
// Only calculate these values when status first encountered
|
||||
// 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.contentHtml = normalOldStatus.get('contentHtml');
|
||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||
|
|
|
@ -14,6 +14,7 @@ import { unescapeHTML } from '../utils/html';
|
|||
import { getFilters, regexFromFilters } from '../selectors';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
|
||||
export const NOTIFICATIONS_INITIALIZE = 'NOTIFICATIONS_INITIALIZE';
|
||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||
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_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||
export const NOTIFICATIONS_MARK_READ = 'NOTIFICATIONS_MARK_READ';
|
||||
|
||||
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) {
|
||||
return (dispatch, getState) => {
|
||||
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
||||
|
@ -134,6 +142,7 @@ export function dequeueNotifications() {
|
|||
dispatch({
|
||||
type: NOTIFICATIONS_DEQUEUE,
|
||||
});
|
||||
dispatch(markReadNotifications());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -225,10 +234,13 @@ export function clearNotifications() {
|
|||
};
|
||||
|
||||
export function scrollTopNotifications(top) {
|
||||
return {
|
||||
type: NOTIFICATIONS_SCROLL_TOP,
|
||||
top,
|
||||
};
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_SCROLL_TOP,
|
||||
top,
|
||||
});
|
||||
dispatch(markReadNotifications());
|
||||
}
|
||||
};
|
||||
|
||||
export function setFilter (filterType) {
|
||||
|
@ -242,3 +254,20 @@ export function setFilter (filterType) {
|
|||
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: {
|
||||
q: value,
|
||||
resolve: true,
|
||||
limit: 5,
|
||||
},
|
||||
}).then(response => {
|
||||
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 { deleteFromTimelines } from './timelines';
|
||||
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
|
||||
import { ensureComposeIsVisible } from './compose';
|
||||
import { openModal } from './modal';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
|
||||
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_HIDE = 'STATUS_HIDE';
|
||||
|
||||
export const REDRAFT = 'REDRAFT';
|
||||
export const STATUS_EDIT = 'STATUS_EDIT';
|
||||
|
||||
export function fetchStatusRequest(id, skipLoading) {
|
||||
return {
|
||||
|
@ -132,15 +132,18 @@ export function fetchStatusFail(id, error, skipLoading) {
|
|||
};
|
||||
};
|
||||
|
||||
export function redraft(status, raw_text) {
|
||||
return {
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
export function editStatus(status) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: STATUS_EDIT,
|
||||
status,
|
||||
});
|
||||
|
||||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||
export function deleteStatus(id, routerHistory) {
|
||||
return (dispatch, getState) => {
|
||||
if (!me) return;
|
||||
|
||||
|
@ -156,11 +159,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
|||
evictStatus(id);
|
||||
dispatch(deleteStatusSuccess(id));
|
||||
dispatch(deleteFromTimelines(id));
|
||||
|
||||
if (withRedraft) {
|
||||
dispatch(redraft(status, response.data.text));
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
}
|
||||
}).catch(error => {
|
||||
dispatch(deleteStatusFail(id, error));
|
||||
});
|
||||
|
@ -272,7 +270,7 @@ export function muteStatusFail(id, error) {
|
|||
export function unmuteStatus(id) {
|
||||
return (dispatch, getState) => {
|
||||
if (!me) return;
|
||||
|
||||
|
||||
dispatch(unmuteStatusRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { updateNotificationsQueue, expandNotifications } from './notifications';
|
|||
import { updateConversations } from './conversations';
|
||||
import { fetchFilters } from './filters';
|
||||
import { getLocale } from '../locales';
|
||||
import { handleComposeSubmit } from './compose';
|
||||
|
||||
const { messages } = getLocale();
|
||||
|
||||
|
@ -61,3 +62,18 @@ export const connectHashtagStream = (id, tag, accept) => connectTimelineStream
|
|||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${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 (
|
||||
<div className='extended-video-player'>
|
||||
<video
|
||||
playsInline
|
||||
ref={this.setRef}
|
||||
src={src}
|
||||
autoPlay
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
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 { isIOS } from '../../utils/is_mobile';
|
||||
import { autoPlayGif, displayMedia } from '../../initial_state';
|
||||
import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio';
|
||||
|
||||
import './media_gallery.scss';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
warning: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' },
|
||||
|
@ -26,6 +28,7 @@ class Item extends ImmutablePureComponent {
|
|||
onClick: PropTypes.func.isRequired,
|
||||
displayWidth: PropTypes.number,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
dimensions: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -51,9 +54,15 @@ class Item extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleLoadedMetaData = (e) => {
|
||||
if (!this.hoverToPlay()) {
|
||||
e.target.play();
|
||||
}
|
||||
}
|
||||
|
||||
hoverToPlay () {
|
||||
const { attachment } = this.props;
|
||||
return !autoPlayGif && attachment.get('type') === 'gifv';
|
||||
return autoPlayGif === false && attachment.get('type') === 'gifv';
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
|
@ -105,43 +114,36 @@ class Item extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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 height = (size === 4 || (size === 3 && index > 0)) ? 50 : 100;
|
||||
const ar = attachment.getIn(['meta', 'small', 'aspect']);
|
||||
|
||||
let top = 'auto';
|
||||
let left = 'auto';
|
||||
let width = 100;
|
||||
let height = '100%';
|
||||
let top = 'auto';
|
||||
let left = 'auto';
|
||||
let bottom = 'auto';
|
||||
let right = 'auto';
|
||||
let right = 'auto';
|
||||
let float = 'left';
|
||||
let position = 'relative';
|
||||
|
||||
switch(size) {
|
||||
case 2:
|
||||
if (index === 0) right = '2px';
|
||||
else left = '2px';
|
||||
break;
|
||||
case 3:
|
||||
if (index === 0) right = '2px';
|
||||
else if (index > 0) left = '2px';
|
||||
|
||||
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;
|
||||
if (dimensions) {
|
||||
width = dimensions.w;
|
||||
height = dimensions.h;
|
||||
top = dimensions.t || 'auto';
|
||||
right = dimensions.r || 'auto';
|
||||
bottom = dimensions.b || 'auto';
|
||||
left = dimensions.l || 'auto';
|
||||
float = dimensions.float || 'left';
|
||||
position = dimensions.pos || 'relative';
|
||||
}
|
||||
|
||||
let thumbnail = '';
|
||||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
return (
|
||||
<div className={classNames('media-item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<a className='media-item__thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<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' />
|
||||
</a>
|
||||
</div>
|
||||
|
@ -182,7 +184,7 @@ class Item extends ImmutablePureComponent {
|
|||
</a>
|
||||
);
|
||||
} else if (attachment.get('type') === 'gifv') {
|
||||
const autoPlay = !isIOS() && autoPlayGif;
|
||||
const autoPlay = !isIOS() && autoPlayGif !== false;
|
||||
|
||||
thumbnail = (
|
||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||
|
@ -195,9 +197,12 @@ class Item extends ImmutablePureComponent {
|
|||
onClick={this.handleClick}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onLoadedMetadata={this.handleLoadedMetaData}
|
||||
autoPlay={autoPlay}
|
||||
type='video/mp4'
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
|
||||
<span className='media-gallery__gifv__label'>GIF</span>
|
||||
|
@ -206,7 +211,7 @@ class Item extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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 })} />
|
||||
{visible && thumbnail}
|
||||
</div>
|
||||
|
@ -262,7 +267,7 @@ class MediaGallery extends PureComponent {
|
|||
}
|
||||
|
||||
handleRef = (node) => {
|
||||
if (node /*&& this.isStandaloneEligible()*/) {
|
||||
if (node) {
|
||||
// offsetWidth triggers a layout, so only calculate when we need to
|
||||
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 () {
|
||||
const { media, intl, sensitive, height, defaultWidth } = this.props;
|
||||
const { visible } = this.state;
|
||||
|
@ -286,24 +286,221 @@ class MediaGallery extends PureComponent {
|
|||
let children, spoilerButton;
|
||||
|
||||
const style = {};
|
||||
const size = media.take(4).size;
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
if (width) {
|
||||
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
const standard169 = width / (16 / 9);
|
||||
const standard169_percent = 100 / (16 / 9);
|
||||
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 {
|
||||
style.height = height;
|
||||
}
|
||||
|
||||
const size = media.take(4).size;
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||
} else {
|
||||
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} />);
|
||||
}
|
||||
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}
|
||||
dimensions={itemsDimensions[i]}
|
||||
/>
|
||||
));
|
||||
|
||||
if (visible) {
|
||||
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' />;
|
||||
}
|
||||
|
||||
const { icon = null, text, meta = null, active = false, href = '#' } = action;
|
||||
const { icon = null, text, meta = null, active = false, href = '#', isLogout } = action;
|
||||
|
||||
return (
|
||||
<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 />}
|
||||
<div>
|
||||
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
||||
|
|
|
@ -10,6 +10,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
composeId: state.getIn(['compose', 'id']),
|
||||
composeText: state.getIn(['compose', 'text']),
|
||||
});
|
||||
|
||||
|
@ -32,6 +33,7 @@ class ModalBase extends PureComponent {
|
|||
onOpenModal: PropTypes.func.isRequired,
|
||||
onCancelReplyCompose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
composeId: PropTypes.string,
|
||||
composeText: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
@ -50,9 +52,9 @@ class ModalBase extends PureComponent {
|
|||
}
|
||||
|
||||
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', {
|
||||
message: <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this status?' />,
|
||||
confirm: intl.formatMessage(messages.confirm),
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
EmbedModal,
|
||||
ListEditor,
|
||||
ListAdder,
|
||||
StatusRevisionModal,
|
||||
} from '../../features/ui/util/async-components';
|
||||
|
||||
const MODAL_COMPONENTS = {
|
||||
|
@ -34,10 +35,12 @@ const MODAL_COMPONENTS = {
|
|||
'EMBED': EmbedModal,
|
||||
'LIST_EDITOR': ListEditor,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||
'LIST_ADDER':ListAdder,
|
||||
'LIST_ADDER': ListAdder,
|
||||
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
|
||||
'STATUS_REVISION': StatusRevisionModal,
|
||||
'COMPOSE': () => Promise.resolve({ default: ComposeModal }),
|
||||
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
|
||||
'PRO_UPGRADE': () => Promise.resolve({ default: ProUpgradeModal }),
|
||||
};
|
||||
|
||||
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 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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
@ -63,9 +77,12 @@ class Status extends ImmutablePureComponent {
|
|||
account: ImmutablePropTypes.map,
|
||||
onClick: PropTypes.func,
|
||||
onReply: PropTypes.func,
|
||||
onShowRevisions: PropTypes.func,
|
||||
onQuote: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onEdit: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
onPin: PropTypes.func,
|
||||
|
@ -86,6 +103,8 @@ class Status extends ImmutablePureComponent {
|
|||
cacheMediaWidth: PropTypes.func,
|
||||
cachedMediaWidth: PropTypes.number,
|
||||
group: ImmutablePropTypes.map,
|
||||
promoted: PropTypes.bool,
|
||||
onOpenProUpgradeModal: PropTypes.func,
|
||||
};
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
|
@ -248,11 +267,16 @@ class Status extends ImmutablePureComponent {
|
|||
this.node = c;
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
handleOpenProUpgradeModal = () => {
|
||||
this.props.onOpenProUpgradeModal();
|
||||
}
|
||||
|
||||
render () {
|
||||
let media = null;
|
||||
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;
|
||||
|
||||
|
@ -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 = (
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend-icon-wrapper'>
|
||||
|
@ -341,6 +372,7 @@ class Status extends ImmutablePureComponent {
|
|||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
inline
|
||||
|
@ -376,7 +408,6 @@ class Status extends ImmutablePureComponent {
|
|||
<Card
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
card={status.get('card')}
|
||||
compact
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
/>
|
||||
|
@ -446,10 +477,10 @@ class Status extends ImmutablePureComponent {
|
|||
</NavLink>
|
||||
</div>
|
||||
|
||||
{!group && status.get('group') && (
|
||||
{((!group && status.get('group')) || status.get('revised_at') !== null) && (
|
||||
<div className='status__meta'>
|
||||
Posted in{' '}
|
||||
<NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink>
|
||||
{!group && status.get('group') && <React.Fragment>Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink></React.Fragment>}
|
||||
{status.get('revised_at') !== null && <a onClick={() => other.onShowRevisions(status)}> Edited</a>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -464,9 +495,11 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
{media}
|
||||
|
||||
{showThread &&
|
||||
status.get('in_reply_to_id') &&
|
||||
status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||
{status.get('quote') && <StatusQuote
|
||||
id={status.get('quote')}
|
||||
/>}
|
||||
|
||||
{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}>
|
||||
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
||||
</button>
|
||||
|
|
|
@ -12,18 +12,20 @@ import './status_action_bar.scss';
|
|||
|
||||
const messages = defineMessages({
|
||||
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}' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
||||
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
||||
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' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
|
@ -57,6 +59,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
status: ImmutablePropTypes.map.isRequired,
|
||||
onOpenUnauthorizedModal: PropTypes.func.isRequired,
|
||||
onReply: PropTypes.func,
|
||||
onQuote: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
|
@ -87,13 +90,12 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleShareClick = () => {
|
||||
navigator.share({
|
||||
text: this.props.status.get('search_index'),
|
||||
url: this.props.status.get('url'),
|
||||
}).catch((e) => {
|
||||
if (e.name !== 'AbortError') console.error(e);
|
||||
});
|
||||
handleQuoteClick = () => {
|
||||
if (me) {
|
||||
this.props.onQuote(this.props.status, this.context.router.history);
|
||||
} else {
|
||||
this.props.onOpenUnauthorizedModal();
|
||||
}
|
||||
}
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
|
@ -116,8 +118,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
this.props.onDelete(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
||||
handleRedraftClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
handleEditClick = () => {
|
||||
this.props.onEdit(this.props.status);
|
||||
}
|
||||
|
||||
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(messages.delete), action: this.handleDeleteClick });
|
||||
menu.push({ text: formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||
} else {
|
||||
menu.push({ text: formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
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>}
|
||||
</div>
|
||||
|
||||
<div className='status-action-bar-item'>
|
||||
<IconButton
|
||||
className='status-action-bar-item__btn star-icon'
|
||||
active={status.get('favourited')}
|
||||
pressed={status.get('favourited')}
|
||||
title={formatMessage(messages.favourite)}
|
||||
icon='star'
|
||||
onClick={this.handleFavouriteClick}
|
||||
/>
|
||||
{favoriteCount !== 0 && <span className='status-action-bar-item__link'>{favoriteCount}</span>}
|
||||
<div className='status__action-bar__counter'>
|
||||
<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} />
|
||||
</div>
|
||||
<div className='status__action-bar__counter'>
|
||||
<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} />
|
||||
{favoriteCount !== 0 && <span className='detailed-status__link'>{favoriteCount}</span>}
|
||||
</div>
|
||||
|
||||
{shareButton}
|
||||
|
||||
<div className='status-action-bar__dropdown'>
|
||||
<DropdownMenuContainer
|
||||
|
|
|
@ -53,6 +53,7 @@ class StatusCheckBox extends ImmutablePureComponent {
|
|||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||
width={239}
|
||||
height={110}
|
||||
inline
|
||||
|
|
|
@ -8,7 +8,7 @@ import Icon from '../icon';
|
|||
|
||||
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 {
|
||||
|
||||
|
@ -44,7 +44,7 @@ export default class StatusContent extends ImmutablePureComponent {
|
|||
}
|
||||
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) {
|
||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||
|
|
|
@ -26,12 +26,24 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
withGroupAdmin: PropTypes.bool,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
promotion: PropTypes.object,
|
||||
promotedStatus: ImmutablePropTypes.map,
|
||||
fetchStatus: PropTypes.func,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.handleDequeueTimeline();
|
||||
this.fetchPromotedStatus();
|
||||
};
|
||||
|
||||
fetchPromotedStatus() {
|
||||
const { promotion, promotedStatus, fetchStatus } = this.props;
|
||||
|
||||
if (promotion && !promotedStatus) {
|
||||
fetchStatus(promotion.status_id);
|
||||
}
|
||||
}
|
||||
|
||||
getFeaturedStatusCount = () => {
|
||||
return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
|
||||
}
|
||||
|
@ -84,30 +96,23 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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) {
|
||||
return ( <ColumnIndicator type='loading' /> );
|
||||
}
|
||||
|
||||
let scrollableContent = null;
|
||||
if (isLoading || statusIds.size > 0) {
|
||||
scrollableContent = statusIds.map((statusId, i) => {
|
||||
if (statusId === null) {
|
||||
return (
|
||||
<LoadMore
|
||||
gap
|
||||
key={'gap:' + statusIds.get(i + 1)}
|
||||
disabled={isLoading}
|
||||
maxId={i > 0 ? statusIds.get(i - 1) : null}
|
||||
onClick={onLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
let scrollableContent = (isLoading || statusIds.size > 0) ? (
|
||||
statusIds.map((statusId, index) => statusId === null ? (
|
||||
<LoadGap
|
||||
key={'gap:' + statusIds.get(index + 1)}
|
||||
disabled={isLoading}
|
||||
maxId={index > 0 ? statusIds.get(index - 1) : null}
|
||||
onClick={onLoadMore}
|
||||
/>
|
||||
) : (
|
||||
<React.Fragment key={statusId}>
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
|
@ -116,9 +121,17 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
withGroupAdmin={withGroupAdmin}
|
||||
showThread
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
{promotedStatus && index === promotion.position && (
|
||||
<StatusContainer
|
||||
id={promotion.status_id}
|
||||
contextType={timelineId}
|
||||
promoted
|
||||
showThread
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))
|
||||
) : null;
|
||||
|
||||
if (scrollableContent && featuredStatusIds) {
|
||||
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 { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
import { connectUserStream } from '../actions/streaming';
|
||||
import {
|
||||
connectUserStream,
|
||||
connectStatusUpdateStream,
|
||||
} from '../actions/streaming';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from '../locales';
|
||||
import initialState from '../initial_state';
|
||||
|
@ -68,6 +71,7 @@ export default class GabSocial extends PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
this.disconnect = store.dispatch(connectUserStream());
|
||||
store.dispatch(connectStatusUpdateStream());
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
replyCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
quoteCompose,
|
||||
} from '../actions/compose';
|
||||
import {
|
||||
reblog,
|
||||
|
@ -18,6 +19,7 @@ import {
|
|||
muteStatus,
|
||||
unmuteStatus,
|
||||
deleteStatus,
|
||||
editStatus,
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
} from '../actions/statuses';
|
||||
|
@ -35,11 +37,11 @@ import {
|
|||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
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' },
|
||||
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?' },
|
||||
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' },
|
||||
});
|
||||
|
||||
|
@ -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) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
|
@ -86,6 +103,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onShowRevisions (status) {
|
||||
dispatch(openModal('STATUS_REVISION', { status }));
|
||||
},
|
||||
|
||||
onFavourite (status) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
|
@ -109,18 +130,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}));
|
||||
},
|
||||
|
||||
onDelete (status, history, withRedraft = false) {
|
||||
onDelete (status, history) {
|
||||
if (!deleteModal) {
|
||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
||||
dispatch(deleteStatus(status.get('id'), history));
|
||||
} else {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history)),
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
onEdit (status) {
|
||||
dispatch(editStatus(status));
|
||||
},
|
||||
|
||||
onDirect (account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
|
@ -183,6 +208,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(groupRemoveStatus(groupId, statusId));
|
||||
},
|
||||
|
||||
onOpenProUpgradeModal() {
|
||||
dispatch(openModal('PRO_UPGRADE'));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import { debounce } from 'lodash';
|
||||
import { me } from '../initial_state';
|
||||
import StatusList from '../components/status_list/status_list';
|
||||
import { me, promotions } from '../initial_state';
|
||||
import { dequeueTimeline } from 'gabsocial/actions/timelines';
|
||||
import { scrollTopTimeline } from '../actions/timelines';
|
||||
import { sample } from 'lodash';
|
||||
import { fetchStatus } from '../actions/statuses';
|
||||
|
||||
const makeGetStatusIds = () => createSelector([
|
||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
||||
(state, { type, id }) => state.getIn(['settings', type], ImmutableMap()),
|
||||
(state, { type, id }) => state.getIn(['timelines', id, 'items'], ImmutableList()),
|
||||
(state) => state.get('statuses'),
|
||||
], (columnSettings, statusIds, statuses) => {
|
||||
return statusIds.filter(id => {
|
||||
|
@ -31,13 +32,16 @@ const makeGetStatusIds = () => createSelector([
|
|||
|
||||
const mapStateToProps = (state, {timelineId}) => {
|
||||
const getStatusIds = makeGetStatusIds();
|
||||
const promotion = promotions.length > 0 && sample(promotions.filter(p => p.timeline_id === timelineId));
|
||||
|
||||
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),
|
||||
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
||||
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(() => {
|
||||
dispatch(scrollTopTimeline(ownProps.timelineId, false));
|
||||
}, 100),
|
||||
fetchStatus(id) {
|
||||
dispatch(fetchStatus(id));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
|
||||
|
|
|
@ -5,9 +5,11 @@ import { autoPlayGif, displayMedia } from 'gabsocial/initial_state';
|
|||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
import { isIOS } from 'gabsocial/utils/is_mobile';
|
||||
import conversations_list_container from '../../direct_timeline/containers/conversations_list_container';
|
||||
|
||||
import './media_item.scss';
|
||||
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -125,8 +127,10 @@ export default class MediaItem extends ImmutablePureComponent {
|
|||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
autoPlay={autoPlay}
|
||||
preload='auto'
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
|
||||
<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.blocks), to: '/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({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick });
|
||||
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 Icon from '../../../../components/icon';
|
||||
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';
|
||||
|
||||
|
||||
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;
|
||||
|
||||
|
@ -29,6 +33,7 @@ const messages = defineMessages({
|
|||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Gab' },
|
||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||
schedulePost: { id: 'compose_form.schedule_post', defaultMessage: 'Schedule Post' }
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -44,6 +49,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
edit: PropTypes.bool.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
spoiler: PropTypes.bool,
|
||||
|
@ -69,6 +75,8 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
autoFocus: PropTypes.bool,
|
||||
group: ImmutablePropTypes.map,
|
||||
isModalOpen: PropTypes.bool,
|
||||
scheduledAt: PropTypes.instanceOf(Date),
|
||||
setScheduledAt: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -93,12 +101,21 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
handleClick = (e) => {
|
||||
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)) {
|
||||
this.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({
|
||||
composeFocused: false,
|
||||
});
|
||||
|
@ -198,7 +215,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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 disabled = this.props.isSubmitting;
|
||||
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);
|
||||
}
|
||||
|
||||
if (scheduledAt) {
|
||||
publishText = intl.formatMessage(messages.schedulePost);
|
||||
}
|
||||
|
||||
const composeClassNames = classNames({
|
||||
'compose-form': true,
|
||||
'condensed': condensed,
|
||||
|
@ -265,20 +286,25 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
{
|
||||
!condensed &&
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadForm />
|
||||
<PollFormContainer />
|
||||
<UploadFormContainer />
|
||||
{!edit && <PollFormContainer />}
|
||||
</div>
|
||||
}
|
||||
</AutosuggestTextbox>
|
||||
|
||||
{quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} />}
|
||||
|
||||
{
|
||||
!condensed &&
|
||||
<div className='compose-form__buttons-wrapper'>
|
||||
<div className='compose-form__buttons'>
|
||||
<UploadButtonContainer />
|
||||
<PollButtonContainer />
|
||||
{!edit && <PollButtonContainer />}
|
||||
<PrivacyDropdownContainer />
|
||||
<SpoilerButtonContainer />
|
||||
<SchedulePostDropdownContainer
|
||||
position={isModalOpen ? 'top' : undefined}
|
||||
/>
|
||||
</div>
|
||||
<CharacterCounter max={maxPostCharacterCount} text={text} />
|
||||
</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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../../../containers/account_container';
|
||||
import StatusContainer from '../../../../containers/status_container';
|
||||
import TrendingItem from '../../../../components/trending_item';
|
||||
import Icon from '../../../../components/icon';
|
||||
import { WhoToFollowPanel } from '../../../../components/panel';
|
||||
import Hashtag from '../../../components/hashtag';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import WhoToFollowPanel from '../../ui/components/who_to_follow_panel';
|
||||
// import TrendsPanel from '../../ui/components/trends_panel';
|
||||
import GroupListItem from 'gabsocial/components/group_list_item';
|
||||
|
||||
import './search_results.scss';
|
||||
|
||||
export default class SearchResults extends ImmutablePureComponent {
|
||||
export default
|
||||
@injectIntl
|
||||
class SearchResults extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
results: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
location: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -20,7 +21,7 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { results } = this.props;
|
||||
const { results, location } = this.props;
|
||||
const { isSmallScreen } = this.state;
|
||||
|
||||
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;
|
||||
|
||||
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 = (
|
||||
<div className='search-results__section'>
|
||||
<h5>
|
||||
<Icon id='users' fixedWidth />
|
||||
<FormattedMessage id='search_results.accounts' defaultMessage='People' />
|
||||
</h5>
|
||||
|
||||
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
<h5><Icon id='user' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
|
||||
{results.get('accounts').slice(0, size).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||
count += results.get('statuses').size;
|
||||
statuses = (
|
||||
|
||||
if (results.get('groups') && results.get('groups').size > 0 && (isTop || showGroups)) {
|
||||
const size = isTop ? Math.min(results.get('groups').size, 5) : results.get('groups').size;
|
||||
count += size;
|
||||
groups = (
|
||||
<div className='search-results__section'>
|
||||
<h5>
|
||||
<Icon id='quote-right' fixedWidth />
|
||||
<FormattedMessage id='search_results.statuses' defaultMessage='Gabs' />
|
||||
</h5>
|
||||
|
||||
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.groups' defaultMessage='Groups' /></h5>
|
||||
{results.get('groups').slice(0, size).map(group => <GroupListItem key={`search-${group.get('name')}`} group={group} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||
count += results.get('hashtags').size;
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0 && (isTop || showHashtags)) {
|
||||
const size = isTop ? Math.min(results.get('hashtags').size, 5) : results.get('hashtags').size;
|
||||
count += size;
|
||||
hashtags = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
||||
|
||||
{results.get('hashtags').map(hashtag => <TrendingItem key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
{results.get('hashtags').slice(0, size).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -87,6 +90,7 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
{accounts}
|
||||
{groups}
|
||||
{statuses}
|
||||
{hashtags}
|
||||
</div>
|
||||
|
|
|
@ -10,8 +10,7 @@ import './upload.scss';
|
|||
|
||||
const messages = defineMessages({
|
||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||
undo: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
||||
focus: { id: 'upload_form.focus', defaultMessage: 'Crop' },
|
||||
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
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'>
|
||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ scale }) => (
|
||||
<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 })}>
|
||||
<IconButton
|
||||
onClick={this.handleUndoClick}
|
||||
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 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 })}>
|
||||
<button className='icon-button' title={intl.formatMessage(messages.delete)} onClick={this.handleUndoClick}><Icon id='times'/></button>
|
||||
{media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
|
||||
</div>
|
||||
|
||||
<div className={classNames('compose-form-upload__description', { active })}>
|
||||
|
|
|
@ -8,9 +8,11 @@ import {
|
|||
changeComposeSpoilerText,
|
||||
insertEmojiCompose,
|
||||
uploadCompose,
|
||||
changeScheduledAt,
|
||||
} from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
edit: state.getIn(['compose', 'id']) !== null,
|
||||
text: state.getIn(['compose', 'text']),
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
|
@ -25,6 +27,8 @@ const mapStateToProps = state => ({
|
|||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
isModalOpen: state.get('modal').modalType === 'COMPOSE',
|
||||
quoteOfId: state.getIn(['compose', 'quote_of_id']),
|
||||
scheduledAt: state.getIn(['compose', 'scheduled_at']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
@ -61,6 +65,9 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
dispatch(insertEmojiCompose(position, data, needsSpace));
|
||||
},
|
||||
|
||||
setScheduledAt (date) {
|
||||
dispatch(changeScheduledAt(date));
|
||||
},
|
||||
});
|
||||
|
||||
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 { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
results: state.getIn(['search', 'results']),
|
||||
|
@ -11,4 +12,4 @@ const mapDispatchToProps = dispatch => ({
|
|||
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 GroupCard from './card';
|
||||
import GroupCreate from '../create';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
||||
create: { id: 'groups.create', defaultMessage: 'Create group' },
|
||||
tab_featured: { id: 'groups.tab_featured', defaultMessage: 'Featured' },
|
||||
tab_member: { id: 'groups.tab_member', defaultMessage: 'Groups you\'re in' },
|
||||
tab_admin: { id: 'groups.tab_admin', defaultMessage: 'Groups you manage' },
|
||||
tab_member: { id: 'groups.tab_member', defaultMessage: 'Member' },
|
||||
tab_admin: { id: 'groups.tab_admin', defaultMessage: 'Manage' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { activeTab }) => ({
|
||||
groupIds: state.getIn(['group_lists', activeTab]),
|
||||
account: state.getIn(['accounts', me]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -42,12 +45,27 @@ class Groups extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleOpenProUpgradeModal = () => {
|
||||
this.props.dispatch(openModal('PRO_UPGRADE'));
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const { intl, activeTab } = this.props;
|
||||
const { intl, activeTab, account, onOpenProUpgradeModal } = this.props;
|
||||
|
||||
const isPro = account.get('is_pro');
|
||||
|
||||
return (
|
||||
<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="column-header__wrapper">
|
||||
|
|
|
@ -3,18 +3,22 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { debounce } from 'lodash';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
import {
|
||||
fetchMembers,
|
||||
expandMembers,
|
||||
fetchMembers,
|
||||
expandMembers,
|
||||
updateRole,
|
||||
createRemovedAccount,
|
||||
} from '../../../actions/groups';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import Column from '../../../components/column';
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
|
||||
const mapStateToProps = (state, { params: { id } }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
|
||||
group: state.getIn(['groups', id]),
|
||||
relationships: state.getIn(['group_relationships', id]),
|
||||
accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -44,24 +48,44 @@ class GroupMembers extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
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 (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
return (
|
||||
<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 (
|
||||
<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 => <AccountContainer key={id} id={id} withNote={false} />)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
if (relationships.get('admin')) {
|
||||
menu = [
|
||||
{ text: 'Remove from group', action: () => dispatch(createRemovedAccount(group.get('id'), id)) },
|
||||
{ text: 'Make administrator', action: () => dispatch(updateRole(group.get('id'), id, 'admin')) },
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group-account-wrapper" key={id}>
|
||||
<AccountContainer id={id} withNote={false} actionIcon="none" onActionClick={() => true} />
|
||||
{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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import StatusListContainer from '../../../containers/status_list_container';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||
import Column from '../../../components/column';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { connectGroupStream } from '../../../actions/streaming';
|
||||
import { expandGroupTimeline } from '../../../actions/timelines';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
import TimelineComposeBlock from '../../../components/timeline_compose_block';
|
||||
import MissingIndicator from '../../../components/missing_indicator';
|
||||
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) => ({
|
||||
group: state.getIn(['groups', props.params.id]),
|
||||
|
@ -31,6 +44,10 @@ class GroupTimeline extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
collapsed: true,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
@ -52,9 +69,15 @@ class GroupTimeline extends ImmutablePureComponent {
|
|||
this.props.dispatch(expandGroupTimeline(id, { maxId }));
|
||||
}
|
||||
|
||||
handleToggleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.setState({ collapsed: !this.state.collapsed });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { columnId, group, relationships } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const { columnId, group, relationships, account, intl } = this.props;
|
||||
const { collapsed } = this.state;
|
||||
const { id } = this.props.params;
|
||||
|
||||
if (typeof group === 'undefined' || !relationships) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
|
@ -69,18 +92,44 @@ class GroupTimeline extends ImmutablePureComponent {
|
|||
<TimelineComposeBlock size={46} group={group} shouldCondense={true} autoFocus={false} />
|
||||
}
|
||||
|
||||
<div className='group__feed'>
|
||||
<StatusListContainer
|
||||
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>
|
||||
);
|
||||
<div className='group__feed'>
|
||||
<div className="column-header__wrapper">
|
||||
<h1 className="column-header">
|
||||
<Link to={`/groups/${id}`} className={classNames('btn grouped active')}>
|
||||
{intl.formatMessage(messages.tabLatest)}
|
||||
</Link>
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
<button
|
||||
className={classNames('column-header__button', { 'active': !collapsed })}
|
||||
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 = {
|
||||
card: ImmutablePropTypes.map,
|
||||
maxDescription: PropTypes.number,
|
||||
onOpenMedia: PropTypes.func.isRequired,
|
||||
compact: PropTypes.bool,
|
||||
defaultWidth: PropTypes.number,
|
||||
cacheWidth: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
maxDescription: 50,
|
||||
compact: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -132,37 +128,52 @@ export default class Card extends ImmutablePureComponent {
|
|||
ref={this.setRef}
|
||||
className='status-card__image status-card-video'
|
||||
dangerouslySetInnerHTML={content}
|
||||
style={{ height }}
|
||||
style={{
|
||||
height,
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { card, maxDescription, compact } = this.props;
|
||||
const { card } = this.props;
|
||||
const { width, embedded } = this.state;
|
||||
|
||||
if (card === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
|
||||
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
|
||||
const maxDescription = 150;
|
||||
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 className = classnames('status-card', { horizontal, compact, 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 ratio = card.get('width') / card.get('height');
|
||||
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||
const className = classnames('status-card', {
|
||||
horizontal,
|
||||
interactive,
|
||||
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 = (
|
||||
<div className='status-card__content'>
|
||||
{title}
|
||||
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
|
||||
<span className='status-card__host'>{provider}</span>
|
||||
{!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
|
||||
<span className='status-card__host'>
|
||||
<Icon id='link' fixedWidth />
|
||||
{' '}
|
||||
{provider}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 embed = '';
|
||||
let thumbnail = card ? <div style={{ backgroundImage: `url(${cardImg})` }} className='status-card__image-image' /> : thumbnail = <div className='status-card__image-image' />;
|
||||
|
||||
if (interactive) {
|
||||
if (embedded) {
|
||||
|
@ -177,7 +188,6 @@ export default class Card extends ImmutablePureComponent {
|
|||
embed = (
|
||||
<div className='status-card__image'>
|
||||
{thumbnail}
|
||||
|
||||
<div className='status-card__actions'>
|
||||
<div>
|
||||
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
|
||||
|
@ -191,10 +201,10 @@ export default class Card extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className={className} ref={this.setRef}>
|
||||
{embed}
|
||||
{!compact && description}
|
||||
{description}
|
||||
</div>
|
||||
);
|
||||
} else if (card.get('image')) {
|
||||
} else if (cardImg) {
|
||||
embed = (
|
||||
<div className='status-card__image'>
|
||||
{thumbnail}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import StatusQuote from '../../../components/status_quote';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { FormattedDate, FormattedNumber } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
@ -32,12 +33,17 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
compact: PropTypes.bool,
|
||||
showMedia: PropTypes.bool,
|
||||
onToggleMediaVisibility: PropTypes.func,
|
||||
onShowRevisions: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
height: null,
|
||||
};
|
||||
|
||||
handleShowRevisions = () => {
|
||||
this.props.onShowRevisions(this.props.status);
|
||||
}
|
||||
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
this.props.onOpenVideo(media, startTime);
|
||||
}
|
||||
|
@ -110,6 +116,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||
width={300}
|
||||
height={150}
|
||||
inline
|
||||
|
@ -185,16 +192,21 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
||||
</NavLink>
|
||||
|
||||
{status.get('group') && (
|
||||
<div className='status__meta'>
|
||||
Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink>
|
||||
</div>
|
||||
)}
|
||||
{(status.get('group') || status.get('revised_at') !== null) && (
|
||||
<div className='status__meta'>
|
||||
{status.get('group') && <React.Fragment>Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink></React.Fragment>}
|
||||
{status.get('revised_at') !== null && <a onClick={this.handleShowRevisions}> Edited</a>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
|
||||
|
||||
{media}
|
||||
|
||||
{status.get('quote') && <StatusQuote
|
||||
id={status.get('quote')}
|
||||
/>}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<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' />
|
||||
|
|
|
@ -10,13 +10,16 @@ import './detailed_status_action_bar.scss';
|
|||
|
||||
const messages = defineMessages({
|
||||
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}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
||||
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
||||
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' },
|
||||
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
|
@ -50,6 +53,7 @@ class ActionBar extends ImmutablePureComponent {
|
|||
status: ImmutablePropTypes.map.isRequired,
|
||||
onReply: PropTypes.func.isRequired,
|
||||
onReblog: PropTypes.func.isRequired,
|
||||
onQuote: PropTypes.func.isRequired,
|
||||
onFavourite: PropTypes.func.isRequired,
|
||||
onDelete: 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 = () => {
|
||||
if (me) {
|
||||
this.props.onFavourite(this.props.status);
|
||||
|
@ -91,8 +103,8 @@ class ActionBar extends ImmutablePureComponent {
|
|||
this.props.onDelete(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
||||
handleRedraftClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
handleEditClick = () => {
|
||||
this.props.onEdit(this.props.status);
|
||||
}
|
||||
|
||||
handleMentionClick = () => {
|
||||
|
@ -176,7 +188,7 @@ class ActionBar extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push(null);
|
||||
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 {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
menu.push(null);
|
||||
|
@ -208,17 +220,11 @@ class ActionBar extends ImmutablePureComponent {
|
|||
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
|
||||
|
||||
return (
|
||||
<div className='detailed-status-action-bar'>
|
||||
<div className='detailed-status-action-bar__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>
|
||||
<div className='detailed-status-action-bar__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 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>
|
||||
|
||||
<div className='detailed-status__action-bar'>
|
||||
<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>
|
||||
<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 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__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||
{shareButton}
|
||||
|
||||
<div className='detailed-status-action-bar__dropdown'>
|
||||
|
|
|
@ -31,8 +31,6 @@ import { showAlertForError } from '../../../actions/alerts';
|
|||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
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' },
|
||||
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?' },
|
||||
|
@ -106,14 +104,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}));
|
||||
},
|
||||
|
||||
onDelete (status, history, withRedraft = false) {
|
||||
onDelete (status, history) {
|
||||
if (!deleteModal) {
|
||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
||||
dispatch(deleteStatus(status.get('id'), history));
|
||||
} else {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
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');
|
||||
}
|
||||
|
||||
export function StatusRevisionModal () {
|
||||
return import(/* webpackChunkName: "modals/mute_modal" */'../components/status_revision_modal');
|
||||
}
|
||||
|
||||
export function ReportModal () {
|
||||
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 isStaff = getMeta('is_staff');
|
||||
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;
|
||||
|
|
|
@ -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.hashtag": "etiqueta",
|
||||
"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_results.accounts": "Xente",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"search_results.hashtags": "Hashtags",
|
||||
|
|
|
@ -1026,7 +1026,7 @@
|
|||
"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"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "tagar",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"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.hashtag": "แฮชแท็ก",
|
||||
"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_results.accounts": "ผู้คน",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "İnsanlar",
|
||||
"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.hashtag": "hashtag",
|
||||
"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_results.accounts": "People",
|
||||
"search_results.hashtags": "Hashtags",
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
COMPOSE_CHANGE,
|
||||
COMPOSE_REPLY,
|
||||
COMPOSE_REPLY_CANCEL,
|
||||
COMPOSE_QUOTE,
|
||||
COMPOSE_DIRECT,
|
||||
COMPOSE_MENTION,
|
||||
COMPOSE_SUBMIT_REQUEST,
|
||||
|
@ -35,16 +36,18 @@ import {
|
|||
COMPOSE_POLL_OPTION_CHANGE,
|
||||
COMPOSE_POLL_OPTION_REMOVE,
|
||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||
COMPOSE_SCHEDULED_AT_CHANGE,
|
||||
} from '../actions/compose';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
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 uuid from '../utils/uuid';
|
||||
import { me } from '../initial_state';
|
||||
import { unescapeHTML } from '../utils/html';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
id: null,
|
||||
mounted: 0,
|
||||
sensitive: false,
|
||||
spoiler: false,
|
||||
|
@ -55,6 +58,7 @@ const initialState = ImmutableMap({
|
|||
caretPosition: null,
|
||||
preselectDate: null,
|
||||
in_reply_to: null,
|
||||
quote_of_id: null,
|
||||
is_composing: false,
|
||||
is_submitting: false,
|
||||
is_changing_upload: false,
|
||||
|
@ -69,6 +73,7 @@ const initialState = ImmutableMap({
|
|||
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
||||
idempotencyKey: null,
|
||||
tagHistory: ImmutableList(),
|
||||
scheduled_at: null,
|
||||
});
|
||||
|
||||
const initialPoll = ImmutableMap({
|
||||
|
@ -89,17 +94,20 @@ function statusToTextMentions(state, status) {
|
|||
|
||||
function clearAll(state) {
|
||||
return state.withMutations(map => {
|
||||
map.set('id', null);
|
||||
map.set('text', '');
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
map.set('is_submitting', false);
|
||||
map.set('is_changing_upload', false);
|
||||
map.set('in_reply_to', null);
|
||||
map.set('quote_of_id', null);
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('sensitive', false);
|
||||
map.update('media_attachments', list => list.clear());
|
||||
map.set('poll', null);
|
||||
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;
|
||||
|
||||
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;
|
||||
|
@ -247,6 +258,24 @@ export default function compose(state = initialState, action) {
|
|||
map.set('preselectDate', new Date());
|
||||
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) {
|
||||
map.set('spoiler', true);
|
||||
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_RESET:
|
||||
return state.withMutations(map => {
|
||||
map.set('id', null);
|
||||
map.set('quote_of_id', null);
|
||||
map.set('in_reply_to', null);
|
||||
map.set('text', '');
|
||||
map.set('spoiler', false);
|
||||
|
@ -265,6 +296,7 @@ export default function compose(state = initialState, action) {
|
|||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('poll', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('scheduled_at', null);
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
return state.set('is_submitting', true);
|
||||
|
@ -329,10 +361,12 @@ export default function compose(state = initialState, action) {
|
|||
|
||||
return item;
|
||||
}));
|
||||
case REDRAFT:
|
||||
case STATUS_EDIT:
|
||||
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('quote_of_id', action.status.get('quote_of_id'));
|
||||
map.set('privacy', action.status.get('visibility'));
|
||||
map.set('media_attachments', action.status.get('media_attachments'));
|
||||
map.set('focusDate', new Date());
|
||||
|
@ -346,14 +380,6 @@ export default function compose(state = initialState, action) {
|
|||
map.set('spoiler', false);
|
||||
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:
|
||||
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));
|
||||
case COMPOSE_POLL_SETTINGS_CHANGE:
|
||||
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:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ import groups from './groups';
|
|||
import group_relationships from './group_relationships';
|
||||
import group_lists from './group_lists';
|
||||
import group_editor from './group_editor';
|
||||
import sidebar from './sidebar';
|
||||
import status_revision_list from './status_revision_list';
|
||||
|
||||
const reducers = {
|
||||
dropdown_menu,
|
||||
|
@ -75,6 +77,8 @@ const reducers = {
|
|||
group_relationships,
|
||||
group_lists,
|
||||
group_editor,
|
||||
sidebar,
|
||||
status_revision_list,
|
||||
};
|
||||
|
||||
export default combineReducers(reducers);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
NOTIFICATIONS_INITIALIZE,
|
||||
NOTIFICATIONS_UPDATE,
|
||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
NOTIFICATIONS_EXPAND_REQUEST,
|
||||
|
@ -9,6 +10,7 @@ import {
|
|||
NOTIFICATIONS_UPDATE_QUEUE,
|
||||
NOTIFICATIONS_DEQUEUE,
|
||||
MAX_QUEUED_NOTIFICATIONS,
|
||||
NOTIFICATIONS_MARK_READ,
|
||||
} from '../actions/notifications';
|
||||
import {
|
||||
ACCOUNT_BLOCK_SUCCESS,
|
||||
|
@ -17,6 +19,7 @@ import {
|
|||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import compareId from '../utils/compare_id';
|
||||
import { unreadCount } from 'gabsocial/initial_state';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
|
@ -26,6 +29,7 @@ const initialState = ImmutableMap({
|
|||
isLoading: false,
|
||||
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
|
||||
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
|
||||
lastRead: -1,
|
||||
});
|
||||
|
||||
const notificationToMap = notification => ImmutableMap({
|
||||
|
@ -126,6 +130,10 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
|
|||
|
||||
export default function notifications(state = initialState, action) {
|
||||
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:
|
||||
return state.set('isLoading', true);
|
||||
case NOTIFICATIONS_EXPAND_FAIL:
|
||||
|
|
|
@ -43,6 +43,7 @@ export default function search(state = initialState, action) {
|
|||
accounts: ImmutableList(action.results.accounts.map(item => item.id)),
|
||||
statuses: ImmutableList(action.results.statuses.map(item => item.id)),
|
||||
hashtags: fromJS(action.results.hashtags),
|
||||
groups: fromJS(action.results.groups),
|
||||
})).set('submitted', true);
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -24,6 +24,12 @@ const initialState = ImmutableMap({
|
|||
}),
|
||||
}),
|
||||
|
||||
group: ImmutableMap({
|
||||
shows: ImmutableMap({
|
||||
reply: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
notifications: ImmutableMap({
|
||||
alerts: ImmutableMap({
|
||||
follow: true,
|
||||
|
@ -57,6 +63,10 @@ const initialState = ImmutableMap({
|
|||
}),
|
||||
|
||||
community: ImmutableMap({
|
||||
other: ImmutableMap({
|
||||
allFediverse: false,
|
||||
onlyMedia: false,
|
||||
}),
|
||||
regex: ImmutableMap({
|
||||
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/group-card';
|
||||
@import 'gabsocial/components/group-detail';
|
||||
@import 'gabsocial/components/group-accounts';
|
||||
@import 'gabsocial/components/group-form';
|
||||
@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/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