Commit 0a3c6cea authored by mgabdev's avatar mgabdev

Merge branch 'develop' of https://code.gab.com/gab/social/gab-social into feature/frontend_refactor

parents c00c202d 1a6afcdd
# 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
\ No newline at end of file
......@@ -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
# 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
......
# 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
......
# 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
......
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
......
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
# 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
\ No newline at end of file
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');