...
 
Commits (258)
# 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 ...@@ -8,21 +8,21 @@ Our goal is to establish the foundation of a federated network of social network
## Project goals ## Project goals
We have diverged from Gab Social in several ways in pursuit of our own goals. We have diverged from Mastodon in several ways in pursuit of our own goals.
1. Node.js has been updated to 10.15.3LTS for hosting the Streaming API in compliance with the Gab Platform. 1. Node.js has been updated to 10.15.3LTS for hosting the Streaming API in compliance with the Gab Platform.
1. Statuses were renamed from 'toots' to 'gabs' 1. Statuses were renamed from 'toots' to 'gabs'
1. The maximum length of a status was increased to 3,000 characters 1. The maximum length of a status was increased to 3,000 characters
1. Advanced media (MP4, WebM, etc.) was limited to PRO subscribers
1. The creation of custom emoji was limited to PRO subscribers
1. The browser client user experience has been significantly altered to match what users of Gab will expect 1. The browser client user experience has been significantly altered to match what users of Gab will expect
1. Features were added to integrate the system with the Gab platform (accessing trends from Dissenter, for example) 1. Features were added to integrate the system with the Gab platform (accessing trends from Dissenter, for example)
1. Groups and group moderation
1. Quote posting
## BTCPay ## BTCPay
In order to make BTC flow work, 3 enviornment variables need to be set: In order to make BTC flow work, 3 environment variables need to be set:
- `BTCPAY_LEGACY_TOKEN`: So called Legacy Tokens can be found in https://btcpay.xxx.com/stores/yyy/Tokens - `BTCPAY_LEGACY_TOKEN`: So called Legacy Tokens can be found in https://btcpay.[yourdomain].com/stores/[yourstore]/Tokens
- `BTCPAY_PUB_KEY`: Public key that is used when creating an access token or pairing https://btcpay.xxx.com/stores/yyy/Tokens/Create - `BTCPAY_PUB_KEY`: Public key that is used when creating an access token or pairing https://btcpay.[yourdomain].com/stores/[yourstore]/Tokens/Create
- `BTCPAY_MERCHANT_TOKEN`: Token created for facade *merchant* - `BTCPAY_MERCHANT_TOKEN`: Token created for facade *merchant*
## Deployment ## Deployment
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject, :verify, :unverify, :add_donor_badge, :remove_donor_badge, :add_investor_badge, :remove_investor_badge, :edit_pro, :save_pro] before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject, :verify, :unverify, :add_donor_badge, :remove_donor_badge, :add_investor_badge, :remove_investor_badge, :edit_pro, :save_pro, :edit, :update]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
...@@ -173,6 +173,22 @@ module Admin ...@@ -173,6 +173,22 @@ module Admin
redirect_to edit_pro_admin_account_path(@account.id) redirect_to edit_pro_admin_account_path(@account.id)
end end
def edit
redirect_to admin_account_path(@account.id) unless @account.local?
@user = @account.user
end
def update
redirect_to admin_account_path(@account.id) unless @account.local?
@user = @account.user
if @user.update(credentials_params)
redirect_to admin_account_path(@account.id), notice: I18n.t('generic.changes_saved_msg')
else
render action: :edit
end
end
private private
def set_account def set_account
...@@ -211,5 +227,14 @@ module Admin ...@@ -211,5 +227,14 @@ module Admin
def pro_params def pro_params
params.require(:account).permit(:is_pro, :pro_expires_at) params.require(:account).permit(:is_pro, :pro_expires_at)
end end
def credentials_params
new_params = params.require(:user).permit(:email, :password, :password_confirmation)
if new_params[:password].blank? && new_params[:password_confirmation].blank?
new_params.delete(:password)
new_params.delete(:password_confirmation)
end
new_params
end
end end
end end
# 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 ...@@ -11,7 +11,12 @@ class Api::V1::AccountByUsernameController < Api::BaseController
end end
def set_account def set_account
@account = Account.find_local!(params[:username]) username, domain = params[:username].split("@")
if domain
@account = Account.find_remote!(username, domain)
else
@account = Account.find_local!(username)
end
end end
def check_account_suspension def check_account_suspension
......
...@@ -20,13 +20,19 @@ class Api::V1::Groups::AccountsController < Api::BaseController ...@@ -20,13 +20,19 @@ class Api::V1::Groups::AccountsController < Api::BaseController
authorize @group, :join? authorize @group, :join?
@group.accounts << current_account @group.accounts << current_account
if current_user.allows_group_in_home_feed?
current_user.force_regeneration!
end
render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships
end end
def update def update
authorize @group, :update_account? authorize @group, :update_account?
GroupAccount.where(group: @group, account_id: current_account.id).update(group_account_params) @account = @group.accounts.find(params[:account_id])
GroupAccount.where(group: @group, account: @account).update(group_account_params)
render_empty render_empty
end end
...@@ -34,6 +40,11 @@ class Api::V1::Groups::AccountsController < Api::BaseController ...@@ -34,6 +40,11 @@ class Api::V1::Groups::AccountsController < Api::BaseController
authorize @group, :leave? authorize @group, :leave?
GroupAccount.where(group: @group, account_id: current_account.id).destroy_all GroupAccount.where(group: @group, account_id: current_account.id).destroy_all
if current_user.allows_group_in_home_feed?
current_user.force_regeneration!
end
render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships
end end
......
...@@ -12,7 +12,7 @@ class Api::V1::GroupsController < Api::BaseController ...@@ -12,7 +12,7 @@ class Api::V1::GroupsController < Api::BaseController
def index def index
case current_tab case current_tab
when 'featured' when 'featured'
@groups = Group.where(is_featured: true).limit(25).all @groups = Group.where(is_featured: true).limit(50).all
when 'member' when 'member'
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).order('group_accounts.unread_count DESC, group_accounts.id DESC').all @groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).order('group_accounts.unread_count DESC, group_accounts.id DESC').all
when 'admin' when 'admin'
...@@ -33,6 +33,8 @@ class Api::V1::GroupsController < Api::BaseController ...@@ -33,6 +33,8 @@ class Api::V1::GroupsController < Api::BaseController
end end
def create def create
authorize :group, :create?
@group = Group.create!(group_params.merge(account: current_account)) @group = Group.create!(group_params.merge(account: current_account))
render json: @group, serializer: REST::GroupSerializer render json: @group, serializer: REST::GroupSerializer
end end
......
...@@ -10,7 +10,7 @@ class Api::V1::MediaController < Api::BaseController ...@@ -10,7 +10,7 @@ class Api::V1::MediaController < Api::BaseController
respond_to :json respond_to :json
def create def create
@media = current_account.media_attachments.create!(media_params) @media = current_account.media_attachments.create!(account: current_account, file: media_params[:file], description: media_params[:description], focus: media_params[:focus])
render json: @media, serializer: REST::MediaAttachmentSerializer render json: @media, serializer: REST::MediaAttachmentSerializer
rescue Paperclip::Errors::NotIdentifiedByImageMagickError rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422 render json: file_type_error, status: 422
......
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::NotificationsController < Api::BaseController class Api::V1::NotificationsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss] before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss, :mark_read]
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss] before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss, :mark_read]
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
...@@ -30,6 +30,11 @@ class Api::V1::NotificationsController < Api::BaseController ...@@ -30,6 +30,11 @@ class Api::V1::NotificationsController < Api::BaseController
render_empty render_empty
end end
def mark_read
current_account.notifications.find(params[:id]).mark_read!
render_empty
end
private private
def load_notifications def load_notifications
......
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController class Api::V1::SearchController < Api::BaseController
RESULTS_LIMIT = 20 RESULTS_LIMIT = 100
respond_to :json respond_to :json
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
class Api::V1::StatusesController < Api::BaseController class Api::V1::StatusesController < Api::BaseController
include Authorization include Authorization
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy] before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
before_action :require_user!, except: [:show, :context, :card] before_action :require_user!, except: [:show, :context, :card]
before_action :set_status, only: [:show, :context, :card] before_action :set_status, only: [:show, :context, :card, :update, :revisions]
respond_to :json respond_to :json
...@@ -33,14 +33,10 @@ class Api::V1::StatusesController < Api::BaseController ...@@ -33,14 +33,10 @@ class Api::V1::StatusesController < Api::BaseController
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
end end
def card def revisions
@card = @status.preview_cards.first @revisions = @status.revisions
if @card.nil? render json: @revisions, each_serializer: REST::StatusRevisionSerializer
render_empty
else
render json: @card, serializer: REST::PreviewCardSerializer
end
end end
def create def create
...@@ -55,11 +51,27 @@ class Api::V1::StatusesController < Api::BaseController ...@@ -55,11 +51,27 @@ class Api::V1::StatusesController < Api::BaseController
application: doorkeeper_token.application, application: doorkeeper_token.application,
poll: status_params[:poll], poll: status_params[:poll],
idempotency: request.headers['Idempotency-Key'], idempotency: request.headers['Idempotency-Key'],
group_id: status_params[:group_id]) group_id: status_params[:group_id],
quote_of_id: status_params[:quote_of_id])
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end end
def update
authorize @status, :update?
@status = EditStatusService.new.call(@status,
text: status_params[:status],
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
visibility: status_params[:visibility],
application: doorkeeper_token.application,
idempotency: request.headers['Idempotency-Key'])
render json: @status, serializer: REST::StatusSerializer
end
def destroy def destroy
@status = Status.where(account_id: current_user.account).find(params[:id]) @status = Status.where(account_id: current_user.account).find(params[:id])
authorize @status, :destroy? authorize @status, :destroy?
...@@ -82,6 +94,7 @@ class Api::V1::StatusesController < Api::BaseController ...@@ -82,6 +94,7 @@ class Api::V1::StatusesController < Api::BaseController
params.permit( params.permit(
:status, :status,
:in_reply_to_id, :in_reply_to_id,
:quote_of_id,
:sensitive, :sensitive,
:spoiler_text, :spoiler_text,
:visibility, :visibility,
......
...@@ -38,7 +38,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController ...@@ -38,7 +38,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController
statuses = group_timeline_statuses.without_replies.paginate_by_id( statuses = group_timeline_statuses.without_replies.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id) params_slice(:max_id, :since_id, :min_id)
) ).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) }
if truthy_param?(:only_media) if truthy_param?(:only_media)
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
......
...@@ -4,6 +4,7 @@ class HomeController < ApplicationController ...@@ -4,6 +4,7 @@ class HomeController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_referrer_policy_header before_action :set_referrer_policy_header
before_action :set_initial_state_json before_action :set_initial_state_json
before_action :set_data_for_meta
def index def index
@body_classes = 'app-body' @body_classes = 'app-body'
...@@ -11,17 +12,40 @@ class HomeController < ApplicationController ...@@ -11,17 +12,40 @@ class HomeController < ApplicationController
private private
def set_data_for_meta
return if find_route_matches
if params[:username].present?
@account = Account.find_local(params[:username])
elsif params[:account_username].present?
@account = Account.find_local(params[:account_username])
if params[:id].present? && !@account.nil?
@status = @account.statuses.find(params[:id])
@stream_entry = @status.stream_entry
@type = @stream_entry.activity_type.downcase
end
end
if request.path.starts_with?('/tags') && params[:tag].present?
@tag = Tag.find_normalized(params[:tag])
end
end
def authenticate_user! def authenticate_user!
return if user_signed_in? return if user_signed_in?
# if no current user, dont allow to navigate to these paths # if no current user, dont allow to navigate to these paths
matches = request.path.match(/\A\/(home|groups|tags|lists|notifications|explore|follow_requests|blocks|domain_blocks|mutes)/) if find_route_matches
if matches
redirect_to(homepage_path) redirect_to(homepage_path)
end end
end end
def find_route_matches
request.path.match(/\A\/(home|groups|lists|notifications|explore|follow_requests|blocks|domain_blocks|mutes)/)
end
def set_initial_state_json def set_initial_state_json
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json @initial_state_json = serializable_resource.to_json
......
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 ...@@ -25,7 +25,7 @@ class Settings::NotificationsController < Settings::BaseController
def user_settings_params def user_settings_params
params.require(:user).permit( params.require(:user).permit(
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account emails_from_gabcom),
interactions: %i(must_be_follower must_be_following must_be_following_dm) interactions: %i(must_be_follower must_be_following must_be_following_dm)
) )
end end
......
...@@ -9,6 +9,7 @@ class Settings::PreferencesController < Settings::BaseController ...@@ -9,6 +9,7 @@ class Settings::PreferencesController < Settings::BaseController
def update def update
user_settings.update(user_settings_params.to_h) user_settings.update(user_settings_params.to_h)
current_user.force_regeneration!
if current_user.update(user_params) if current_user.update(user_params)
I18n.locale = current_user.locale I18n.locale = current_user.locale
...@@ -51,7 +52,7 @@ class Settings::PreferencesController < Settings::BaseController ...@@ -51,7 +52,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_show_application, :setting_show_application,
:setting_advanced_layout, :setting_advanced_layout,
:setting_group_in_home_feed, :setting_group_in_home_feed,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account emails_from_gabcom),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following)
) )
end end
......
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 api from '../api';
import { CancelToken, isCancel } from 'axios'; import { CancelToken, isCancel } from 'axios';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import moment from 'moment';
import { search as emojiSearch } from '../components/emoji/emoji_mart_search_light'; import { search as emojiSearch } from '../components/emoji/emoji_mart_search_light';
import { tagHistory } from '../settings'; import { tagHistory } from '../settings';
import { useEmoji } from './emojis'; import { useEmoji } from './emojis';
...@@ -20,6 +21,7 @@ export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; ...@@ -20,6 +21,7 @@ export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_MENTION = 'COMPOSE_MENTION';
...@@ -60,6 +62,8 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; ...@@ -60,6 +62,8 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
export const COMPOSE_SCHEDULED_AT_CHANGE = 'COMPOSE_SCHEDULED_AT_CHANGE';
const messages = defineMessages({ const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
...@@ -91,6 +95,17 @@ export function replyCompose(status, routerHistory) { ...@@ -91,6 +95,17 @@ export function replyCompose(status, routerHistory) {
}; };
}; };
export function quoteCompose(status, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_QUOTE,
status: status,
});
dispatch(openModal('COMPOSE'));
};
};
export function cancelReplyCompose() { export function cancelReplyCompose() {
return { return {
type: COMPOSE_REPLY_CANCEL, type: COMPOSE_REPLY_CANCEL,
...@@ -125,6 +140,45 @@ export function directCompose(account, routerHistory) { ...@@ -125,6 +140,45 @@ export function directCompose(account, routerHistory) {
}; };
}; };
export function handleComposeSubmit(dispatch, getState, response, status) {
if (!dispatch || !getState) return;
const isScheduledStatus = response.data['scheduled_at'] !== undefined;
if (isScheduledStatus) {
dispatch(showAlertForError({
response: {
data: {},
status: 200,
statusText: 'Successfully scheduled status',
}
}));
dispatch(submitComposeSuccess({ ...response.data }));
return;
}
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately push the status into the columns
const insertIfOnline = timelineId => {
const timeline = getState().getIn(['timelines', timelineId]);
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
let dequeueArgs = {};
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']);
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};
if (response.data.visibility !== 'direct') {
insertIfOnline('home');
} else if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
insertIfOnline('public');
}
}
export function submitCompose(routerHistory, group) { export function submitCompose(routerHistory, group) {
return function (dispatch, getState) { return function (dispatch, getState) {
if (!me) return; if (!me) return;
...@@ -139,9 +193,20 @@ export function submitCompose(routerHistory, group) { ...@@ -139,9 +193,20 @@ export function submitCompose(routerHistory, group) {
dispatch(submitComposeRequest()); dispatch(submitComposeRequest());
dispatch(closeModal()); dispatch(closeModal());
api(getState).post('/api/v1/statuses', { const id = getState().getIn(['compose', 'id']);
const endpoint = id === null
? '/api/v1/statuses'
: `/api/v1/statuses/${id}`;
const method = id === null ? 'post' : 'put';
let scheduled_at = getState().getIn(['compose', 'scheduled_at'], null);
if (scheduled_at !== null) scheduled_at = moment.utc(scheduled_at).toDate();
api(getState)[method](endpoint, {
status, status,
scheduled_at,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
quote_of_id: getState().getIn(['compose', 'quote_of_id'], null),
media_ids: media.map(item => item.get('id')), media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']), sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
...@@ -156,33 +221,7 @@ export function submitCompose(routerHistory, group) { ...@@ -156,33 +221,7 @@ export function submitCompose(routerHistory, group) {
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) { if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
routerHistory.push('/messages'); routerHistory.push('/messages');
} }
handleComposeSubmit(dispatch, getState, response, status);
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately push the status
// into the columns
const insertIfOnline = timelineId => {
const timeline = getState().getIn(['timelines', timelineId]);
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
let dequeueArgs = {};
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']),
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};
if (response.data.visibility !== 'direct') {
insertIfOnline('home');
}
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
insertIfOnline('public');
}
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));
}); });
...@@ -561,3 +600,10 @@ export function changePollSettings(expiresIn, isMultiple) { ...@@ -561,3 +600,10 @@ export function changePollSettings(expiresIn, isMultiple) {
isMultiple, isMultiple,
}; };
}; };
export function changeScheduledAt(date) {
return {
type: COMPOSE_SCHEDULED_AT_CHANGE,
date,
};
};
\ No newline at end of file
...@@ -51,6 +51,10 @@ export const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST'; ...@@ -51,6 +51,10 @@ export const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST';
export const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS'; export const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL'; export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL';
export const GROUP_UPDATE_ROLE_REQUEST = 'GROUP_UPDATE_ROLE_REQUEST';
export const GROUP_UPDATE_ROLE_SUCCESS = 'GROUP_UPDATE_ROLE_SUCCESS';
export const GROUP_UPDATE_ROLE_FAIL = 'GROUP_UPDATE_ROLE_FAIL';
export const fetchGroup = id => (dispatch, getState) => { export const fetchGroup = id => (dispatch, getState) => {
if (!me) return; if (!me) return;
...@@ -521,4 +525,43 @@ export function groupRemoveStatusFail(groupId, id, error) { ...@@ -521,4 +525,43 @@ export function groupRemoveStatusFail(groupId, id, error) {
id, id,
error, error,
}; };
};
export function updateRole(groupId, id, role) {
return (dispatch, getState) => {
if (!me) return;
dispatch(updateRoleRequest(groupId, id));
api(getState).patch(`/api/v1/groups/${groupId}/accounts?account_id=${id}`, { role }).then(response => {
dispatch(updateRoleSuccess(groupId, id));
}).catch(error => {
dispatch(updateRoleFail(groupId, id, error));
});
};
};
export function updateRoleRequest(groupId, id) {
return {
type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
groupId,
id,
};
};
export function updateRoleSuccess(groupId, id) {
return {
type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
groupId,
id,
};
};
export function updateRoleFail(groupId, id, error) {
return {
type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
groupId,
id,
error,
};
}; };
\ No newline at end of file
...@@ -71,6 +71,10 @@ export function importFetchedStatuses(statuses) { ...@@ -71,6 +71,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog); processStatus(status.reblog);
} }
if (status.quote && status.quote.id) {
processStatus(status.quote);
}
if (status.poll && status.poll.id) { if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll)); pushUnique(polls, normalizePoll(status.poll));
} }
......
...@@ -43,13 +43,17 @@ export function normalizeStatus(status, normalOldStatus) { ...@@ -43,13 +43,17 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.reblog = status.reblog.id; normalStatus.reblog = status.reblog.id;
} }
if (status.quote && status.quote.id) {
normalStatus.quote = status.quote.id;
}
if (status.poll && status.poll.id) { if (status.poll && status.poll.id) {
normalStatus.poll = status.poll.id; normalStatus.poll = status.poll.id;
} }
// Only calculate these values when status first encountered // Only calculate these values when status first encountered
// Otherwise keep the ones already in the reducer // Otherwise keep the ones already in the reducer
if (normalOldStatus) { if (normalOldStatus && normalOldStatus.get('content') === normalStatus.content && normalOldStatus.get('spoiler_text') === normalStatus.spoiler_text) {
normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.search_index = normalOldStatus.get('search_index');
normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
......
...@@ -14,6 +14,7 @@ import { unescapeHTML } from '../utils/html'; ...@@ -14,6 +14,7 @@ import { unescapeHTML } from '../utils/html';
import { getFilters, regexFromFilters } from '../selectors'; import { getFilters, regexFromFilters } from '../selectors';
import { me } from 'gabsocial/initial_state'; import { me } from 'gabsocial/initial_state';
export const NOTIFICATIONS_INITIALIZE = 'NOTIFICATIONS_INITIALIZE';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE'; export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
...@@ -27,6 +28,7 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; ...@@ -27,6 +28,7 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const NOTIFICATIONS_MARK_READ = 'NOTIFICATIONS_MARK_READ';
export const MAX_QUEUED_NOTIFICATIONS = 40; export const MAX_QUEUED_NOTIFICATIONS = 40;
...@@ -43,6 +45,12 @@ const fetchRelatedRelationships = (dispatch, notifications) => { ...@@ -43,6 +45,12 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
} }
}; };
export function initializeNotifications() {
return {
type: NOTIFICATIONS_INITIALIZE,
};
}
export function updateNotifications(notification, intlMessages, intlLocale) { export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => { return (dispatch, getState) => {
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
...@@ -134,6 +142,7 @@ export function dequeueNotifications() { ...@@ -134,6 +142,7 @@ export function dequeueNotifications() {
dispatch({ dispatch({
type: NOTIFICATIONS_DEQUEUE, type: NOTIFICATIONS_DEQUEUE,
}); });
dispatch(markReadNotifications());
} }
}; };
...@@ -225,10 +234,13 @@ export function clearNotifications() { ...@@ -225,10 +234,13 @@ export function clearNotifications() {
}; };
export function scrollTopNotifications(top) { export function scrollTopNotifications(top) {
return { return (dispatch, getState) => {
type: NOTIFICATIONS_SCROLL_TOP, dispatch({
top, type: NOTIFICATIONS_SCROLL_TOP,
}; top,
});
dispatch(markReadNotifications());
}
}; };
export function setFilter (filterType) { export function setFilter (filterType) {
...@@ -242,3 +254,20 @@ export function setFilter (filterType) { ...@@ -242,3 +254,20 @@ export function setFilter (filterType) {
dispatch(saveSettings()); dispatch(saveSettings());
}; };
}; };
export function markReadNotifications() {
return (dispatch, getState) => {
if (!me) return;
const top_notification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']));
const last_read = getState().getIn(['notifications', 'lastRead']);
if (top_notification && top_notification > last_read) {
api(getState).post('/api/v1/notifications/mark_read', {id: top_notification}).then(response => {
dispatch({
type: NOTIFICATIONS_MARK_READ,
notification: top_notification,
});
});
}
}
};
\ No newline at end of file
...@@ -37,7 +37,6 @@ export function submitSearch() { ...@@ -37,7 +37,6 @@ export function submitSearch() {
params: { params: {
q: value, q: value,
resolve: true, resolve: true,
limit: 5,
}, },
}).then(response => { }).then(response => {
if (response.data.accounts) { if (response.data.accounts) {
......
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,
};
};
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)));
};
}
\ No newline at end of file
...@@ -3,7 +3,7 @@ import openDB from '../storage/db'; ...@@ -3,7 +3,7 @@ import openDB from '../storage/db';
import { evictStatus } from '../storage/modifier'; import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
import { ensureComposeIsVisible } from './compose'; import { openModal } from './modal';
import { me } from 'gabsocial/initial_state'; import { me } from 'gabsocial/initial_state';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
...@@ -29,7 +29,7 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; ...@@ -29,7 +29,7 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_REVEAL = 'STATUS_REVEAL'; export const STATUS_REVEAL = 'STATUS_REVEAL';
export const STATUS_HIDE = 'STATUS_HIDE'; export const STATUS_HIDE = 'STATUS_HIDE';
export const REDRAFT = 'REDRAFT'; export const STATUS_EDIT = 'STATUS_EDIT';
export function fetchStatusRequest(id, skipLoading) { export function fetchStatusRequest(id, skipLoading) {
return { return {
...@@ -132,15 +132,18 @@ export function fetchStatusFail(id, error, skipLoading) { ...@@ -132,15 +132,18 @@ export function fetchStatusFail(id, error, skipLoading) {
}; };
}; };
export function redraft(status, raw_text) { export function editStatus(status) {
return { return dispatch => {
type: REDRAFT, dispatch({
status, type: STATUS_EDIT,
raw_text, status,
});
dispatch(openModal('COMPOSE'));
}; };
}; };
export function deleteStatus(id, routerHistory, withRedraft = false) { export function deleteStatus(id, routerHistory) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!me) return; if (!me) return;
...@@ -156,11 +159,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { ...@@ -156,11 +159,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
evictStatus(id); evictStatus(id);
dispatch(deleteStatusSuccess(id)); dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id)); dispatch(deleteFromTimelines(id));
if (withRedraft) {
dispatch(redraft(status, response.data.text));
ensureComposeIsVisible(getState, routerHistory);
}
}).catch(error => { }).catch(error => {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));
}); });
...@@ -272,7 +270,7 @@ export function muteStatusFail(id, error) { ...@@ -272,7 +270,7 @@ export function muteStatusFail(id, error) {
export function unmuteStatus(id) { export function unmuteStatus(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!me) return; if (!me) return;
dispatch(unmuteStatusRequest(id)); dispatch(unmuteStatusRequest(id));
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => { api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
......
...@@ -10,6 +10,7 @@ import { updateNotificationsQueue, expandNotifications } from './notifications'; ...@@ -10,6 +10,7 @@ import { updateNotificationsQueue, expandNotifications } from './notifications';
import { updateConversations } from './conversations'; import { updateConversations } from './conversations';
import { fetchFilters } from './filters'; import { fetchFilters } from './filters';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
import { handleComposeSubmit } from './compose';
const { messages } = getLocale(); const { messages } = getLocale();
...@@ -61,3 +62,18 @@ export const connectHashtagStream = (id, tag, accept) => connectTimelineStream ...@@ -61,3 +62,18 @@ export const connectHashtagStream = (id, tag, accept) => connectTimelineStream
export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`); export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
export const connectStatusUpdateStream = () => {
return connectStream('statuscard', null, (dispatch, getState) => {
return {
onConnect() {},
onDisconnect() {},
onReceive (data) {
if (!data['event'] || !data['payload']) return;
if (data.event === 'update') {
handleComposeSubmit(dispatch, getState, {data: JSON.parse(data.payload)}, null)
}
},
};
});
}
\ No newline at end of file
...@@ -43,9 +43,9 @@ export default class ExtendedVideoPlayer extends PureComponent { ...@@ -43,9 +43,9 @@ export default class ExtendedVideoPlayer extends PureComponent {
return ( return (
<div className='extended-video-player'> <div className='extended-video-player'>
<video <video
playsInline
ref={this.setRef} ref={this.setRef}
src={src} src={src}
autoPlay
role='button' role='button'
tabIndex='0' tabIndex='0'
aria-label={alt} aria-label={alt}
......
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'))}
&nbsp;
{intl.formatMessage(messages.members)}
</span>
</Link>
</div>
</div>
);
}
}
\ No newline at end of file