Merge branch 'groups-updates' of https://code.gab.com/gab/social/gab-social into develop
This commit is contained in:
commit
46bec5710f
@ -9,7 +9,7 @@ class Api::V1::Groups::AccountsController < Api::BaseController
|
|||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_group
|
before_action :set_group
|
||||||
|
|
||||||
after_action :insert_pagination_headers, only: :index
|
after_action :insert_pagination_headers, only: :show
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@accounts = load_accounts
|
@accounts = load_accounts
|
||||||
|
90
app/controllers/api/v1/groups/removed_accounts_controller.rb
Normal file
90
app/controllers/api/v1/groups/removed_accounts_controller.rb
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Groups::RemovedAccountsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:groups' }
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_group
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: :show
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @group, :show_removed_accounts?
|
||||||
|
|
||||||
|
@accounts = load_accounts
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize @group, :create_removed_account?
|
||||||
|
|
||||||
|
@account = @group.accounts.find(params[:account_id])
|
||||||
|
@group.removed_accounts << @account
|
||||||
|
GroupAccount.where(group: @group, account: @account).destroy_all
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @group, :destroy_removed_account?
|
||||||
|
|
||||||
|
@account = @group.removed_accounts.find(params[:account_id])
|
||||||
|
GroupRemovedAccount.where(group: @group, account: @account).destroy_all
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_group
|
||||||
|
@group = Group.find(params[:group_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_accounts
|
||||||
|
if unlimited?
|
||||||
|
@group.removed_accounts.includes(:account_stat).all
|
||||||
|
else
|
||||||
|
@group.removed_accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
return if unlimited?
|
||||||
|
|
||||||
|
if records_continue?
|
||||||
|
api_v1_group_removed_accounts_url pagination_params(max_id: pagination_max_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
return if unlimited?
|
||||||
|
|
||||||
|
unless @accounts.empty?
|
||||||
|
api_v1_group_removed_accounts_url pagination_params(since_id: pagination_since_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@accounts.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@accounts.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(:limit).permit(:limit).merge(core_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unlimited?
|
||||||
|
params[:limit] == '0'
|
||||||
|
end
|
||||||
|
end
|
@ -10,10 +10,24 @@ class Api::V1::GroupsController < Api::BaseController
|
|||||||
before_action :set_group, except: [:index, :create]
|
before_action :set_group, except: [:index, :create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).all
|
case current_tab
|
||||||
|
when 'featured'
|
||||||
|
@groups = Group.where(is_featured: true).limit(25).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'
|
||||||
|
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account, role: :admin }).all
|
||||||
|
end
|
||||||
|
|
||||||
render json: @groups, each_serializer: REST::GroupSerializer
|
render json: @groups, each_serializer: REST::GroupSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_tab
|
||||||
|
tab = 'featured'
|
||||||
|
tab = params[:tab] if ['featured', 'member', 'admin'].include? params[:tab]
|
||||||
|
return tab
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @group, serializer: REST::GroupSerializer
|
render json: @group, serializer: REST::GroupSerializer
|
||||||
end
|
end
|
||||||
|
@ -9,6 +9,8 @@ class Api::V1::Timelines::GroupController < Api::BaseController
|
|||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
mark_as_unread
|
||||||
|
|
||||||
render json: @statuses,
|
render json: @statuses,
|
||||||
each_serializer: REST::StatusSerializer,
|
each_serializer: REST::StatusSerializer,
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
|
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
|
||||||
@ -16,6 +18,10 @@ class Api::V1::Timelines::GroupController < Api::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def mark_as_unread
|
||||||
|
GroupAccount.where(group: @group, account: current_account).update_all("unread_count = 0")
|
||||||
|
end
|
||||||
|
|
||||||
def set_group
|
def set_group
|
||||||
@group = Group.find(params[:id])
|
@group = Group.find(params[:id])
|
||||||
end
|
end
|
||||||
@ -29,7 +35,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def group_statuses
|
def group_statuses
|
||||||
statuses = tag_timeline_statuses.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)
|
||||||
)
|
)
|
||||||
@ -43,7 +49,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def group_statuses
|
def group_timeline_statuses
|
||||||
GroupQueryService.new.call(@group)
|
GroupQueryService.new.call(@group)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||||||
:setting_aggregate_reblogs,
|
:setting_aggregate_reblogs,
|
||||||
:setting_show_application,
|
:setting_show_application,
|
||||||
:setting_advanced_layout,
|
: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),
|
||||||
interactions: %i(must_be_follower must_be_following)
|
interactions: %i(must_be_follower must_be_following)
|
||||||
)
|
)
|
||||||
|
@ -125,7 +125,7 @@ export function directCompose(account, routerHistory) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function submitCompose(routerHistory) {
|
export function submitCompose(routerHistory, group) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
@ -147,6 +147,7 @@ export function submitCompose(routerHistory) {
|
|||||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||||
visibility: getState().getIn(['compose', 'privacy']),
|
visibility: getState().getIn(['compose', 'privacy']),
|
||||||
poll: getState().getIn(['compose', 'poll'], null),
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
|
group_id: group ? group.get('id') : null,
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
headers: {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
|
113
app/javascript/gabsocial/actions/group_editor.js
Normal file
113
app/javascript/gabsocial/actions/group_editor.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import api from '../api';
|
||||||
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
|
||||||
|
export const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
|
||||||
|
export const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
|
||||||
|
export const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST';
|
||||||
|
export const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS';
|
||||||
|
export const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE';
|
||||||
|
export const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET';
|
||||||
|
export const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP';
|
||||||
|
|
||||||
|
export const submit = (routerHistory) => (dispatch, getState) => {
|
||||||
|
const groupId = getState().getIn(['group_editor', 'groupId']);
|
||||||
|
const title = getState().getIn(['group_editor', 'title']);
|
||||||
|
const description = getState().getIn(['group_editor', 'description']);
|
||||||
|
const coverImage = getState().getIn(['group_editor', 'coverImage']);
|
||||||
|
|
||||||
|
if (groupId === null) {
|
||||||
|
dispatch(create(title, description, coverImage, routerHistory));
|
||||||
|
} else {
|
||||||
|
dispatch(update(groupId, title, description, coverImage, routerHistory));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const create = (title, description, coverImage, routerHistory) => (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
dispatch(createRequest());
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('title', title);
|
||||||
|
formData.append('description', description);
|
||||||
|
|
||||||
|
if (coverImage !== null) {
|
||||||
|
formData.append('cover_image', coverImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
|
||||||
|
dispatch(createSuccess(data));
|
||||||
|
routerHistory.push(`/groups/${data.id}`);
|
||||||
|
}).catch(err => dispatch(createFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const createRequest = id => ({
|
||||||
|
type: GROUP_CREATE_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSuccess = group => ({
|
||||||
|
type: GROUP_CREATE_SUCCESS,
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createFail = error => ({
|
||||||
|
type: GROUP_CREATE_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const update = (groupId, title, description, coverImage, routerHistory) => (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
dispatch(updateRequest());
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('title', title);
|
||||||
|
formData.append('description', description);
|
||||||
|
|
||||||
|
if (coverImage !== null) {
|
||||||
|
formData.append('cover_image', coverImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
|
||||||
|
dispatch(updateSuccess(data));
|
||||||
|
routerHistory.push(`/groups/${data.id}`);
|
||||||
|
}).catch(err => dispatch(updateFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const updateRequest = id => ({
|
||||||
|
type: GROUP_UPDATE_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateSuccess = group => ({
|
||||||
|
type: GROUP_UPDATE_SUCCESS,
|
||||||
|
group,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateFail = error => ({
|
||||||
|
type: GROUP_UPDATE_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const changeValue = (field, value) => ({
|
||||||
|
type: GROUP_EDITOR_VALUE_CHANGE,
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const reset = () => ({
|
||||||
|
type: GROUP_EDITOR_RESET
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setUp = (group) => ({
|
||||||
|
type: GROUP_EDITOR_SETUP,
|
||||||
|
group,
|
||||||
|
});
|
@ -1,5 +1,7 @@
|
|||||||
import api from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { me } from 'gabsocial/initial_state';
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
|
|
||||||
export const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
|
export const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
|
||||||
export const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
|
export const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
|
||||||
@ -21,6 +23,34 @@ export const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
|
|||||||
export const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
|
export const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
|
||||||
export const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
|
export const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST';
|
||||||
|
export const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS';
|
||||||
|
export const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST';
|
||||||
|
export const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS';
|
||||||
|
export const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST';
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS';
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST';
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS';
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST';
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS';
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST';
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS';
|
||||||
|
export const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL';
|
||||||
|
|
||||||
|
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 fetchGroup = id => (dispatch, getState) => {
|
export const fetchGroup = id => (dispatch, getState) => {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
@ -98,13 +128,16 @@ export function fetchGroupRelationshipsFail(error) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchGroups = () => (dispatch, getState) => {
|
export const fetchGroups = (tab) => (dispatch, getState) => {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
dispatch(fetchGroupsRequest());
|
dispatch(fetchGroupsRequest());
|
||||||
|
|
||||||
api(getState).get('/api/v1/groups')
|
api(getState).get('/api/v1/groups?tab=' + tab)
|
||||||
.then(({ data }) => dispatch(fetchGroupsSuccess(data)))
|
.then(({ data }) => {
|
||||||
|
dispatch(fetchGroupsSuccess(data, tab));
|
||||||
|
dispatch(fetchGroupRelationships(data.map(item => item.id)));
|
||||||
|
})
|
||||||
.catch(err => dispatch(fetchGroupsFail(err)));
|
.catch(err => dispatch(fetchGroupsFail(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,9 +145,10 @@ export const fetchGroupsRequest = () => ({
|
|||||||
type: GROUPS_FETCH_REQUEST,
|
type: GROUPS_FETCH_REQUEST,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchGroupsSuccess = groups => ({
|
export const fetchGroupsSuccess = (groups, tab) => ({
|
||||||
type: GROUPS_FETCH_SUCCESS,
|
type: GROUPS_FETCH_SUCCESS,
|
||||||
groups,
|
groups,
|
||||||
|
tab,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchGroupsFail = error => ({
|
export const fetchGroupsFail = error => ({
|
||||||
@ -191,3 +225,300 @@ export function leaveGroupFail(error) {
|
|||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function fetchMembers(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
dispatch(fetchMembersRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchMembersFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMembersRequest(id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_MEMBERS_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMembersSuccess(id, accounts, next) {
|
||||||
|
return {
|
||||||
|
type: GROUP_MEMBERS_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMembersFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: GROUP_MEMBERS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandMembers(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
const url = getState().getIn(['user_lists', 'groups', id, 'next']);
|
||||||
|
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandMembersRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandMembersFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandMembersRequest(id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_MEMBERS_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandMembersSuccess(id, accounts, next) {
|
||||||
|
return {
|
||||||
|
type: GROUP_MEMBERS_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandMembersFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: GROUP_MEMBERS_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchRemovedAccounts(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
dispatch(fetchRemovedAccountsRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchRemovedAccountsFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchRemovedAccountsRequest(id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchRemovedAccountsSuccess(id, accounts, next) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchRemovedAccountsFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandRemovedAccounts(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
const url = getState().getIn(['user_lists', 'groups_removed_accounts', id, 'next']);
|
||||||
|
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandRemovedAccountsRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandRemovedAccountsFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandRemovedAccountsRequest(id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandRemovedAccountsSuccess(id, accounts, next) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandRemovedAccountsFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function removeRemovedAccount(groupId, id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
dispatch(removeRemovedAccountRequest(groupId, id));
|
||||||
|
|
||||||
|
api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
|
||||||
|
dispatch(removeRemovedAccountSuccess(groupId, id));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(removeRemovedAccountFail(groupId, id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function removeRemovedAccountRequest(groupId, id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function removeRemovedAccountSuccess(groupId, id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function removeRemovedAccountFail(groupId, id, error) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createRemovedAccount(groupId, id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
dispatch(createRemovedAccountRequest(groupId, id));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
|
||||||
|
dispatch(createRemovedAccountSuccess(groupId, id));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(createRemovedAccountFail(groupId, id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createRemovedAccountRequest(groupId, id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createRemovedAccountSuccess(groupId, id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createRemovedAccountFail(groupId, id, error) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function groupRemoveStatus(groupId, id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
dispatch(groupRemoveStatusRequest(groupId, id));
|
||||||
|
|
||||||
|
api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => {
|
||||||
|
dispatch(groupRemoveStatusSuccess(groupId, id));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(groupRemoveStatusFail(groupId, id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function groupRemoveStatusRequest(groupId, id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVE_STATUS_REQUEST,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function groupRemoveStatusSuccess(groupId, id) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVE_STATUS_SUCCESS,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function groupRemoveStatusFail(groupId, id, error) {
|
||||||
|
return {
|
||||||
|
type: GROUP_REMOVE_STATUS_FAIL,
|
||||||
|
groupId,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
@ -36,6 +37,8 @@ const messages = defineMessages({
|
|||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||||
|
group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
|
||||||
|
group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove status from group' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class StatusActionBar extends ImmutablePureComponent {
|
class StatusActionBar extends ImmutablePureComponent {
|
||||||
@ -59,6 +62,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
onMuteConversation: PropTypes.func,
|
onMuteConversation: PropTypes.func,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
|
withGroupAdmin: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -165,8 +169,20 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleGroupRemoveAccount = () => {
|
||||||
|
const { status } = this.props;
|
||||||
|
|
||||||
|
this.props.onGroupRemoveAccount(status.get('group_id'), status.getIn(['account', 'id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleGroupRemovePost = () => {
|
||||||
|
const { status } = this.props;
|
||||||
|
|
||||||
|
this.props.onGroupRemoveStatus(status.get('group_id'), status.get('id'));
|
||||||
|
}
|
||||||
|
|
||||||
_makeMenu = (publicStatus) => {
|
_makeMenu = (publicStatus) => {
|
||||||
const { status, intl, withDismiss } = this.props;
|
const { status, intl, withDismiss, withGroupAdmin } = this.props;
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
@ -213,6 +229,12 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
|
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withGroupAdmin) {
|
||||||
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(messages.group_remove_account), action: this.handleGroupRemoveAccount });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.group_remove_post), action: this.handleGroupRemovePost });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return menu;
|
return menu;
|
||||||
|
@ -25,6 +25,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
timelineId: PropTypes.string,
|
timelineId: PropTypes.string,
|
||||||
queuedItemSize: PropTypes.number,
|
queuedItemSize: PropTypes.number,
|
||||||
onDequeueTimeline: PropTypes.func,
|
onDequeueTimeline: PropTypes.func,
|
||||||
|
withGroupAdmin: PropTypes.bool,
|
||||||
onScrollToTop: PropTypes.func,
|
onScrollToTop: PropTypes.func,
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
};
|
};
|
||||||
@ -84,7 +85,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, ...other } = this.props;
|
const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, ...other } = this.props;
|
||||||
|
|
||||||
if (isPartial) {
|
if (isPartial) {
|
||||||
return (
|
return (
|
||||||
@ -114,6 +115,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
|
withGroupAdmin={withGroupAdmin}
|
||||||
showThread
|
showThread
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -29,6 +29,10 @@ import { openModal } from '../actions/modal';
|
|||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { boostModal, deleteModal } from '../initial_state';
|
import { boostModal, deleteModal } from '../initial_state';
|
||||||
import { showAlertForError } from '../actions/alerts';
|
import { showAlertForError } from '../actions/alerts';
|
||||||
|
import {
|
||||||
|
createRemovedAccount,
|
||||||
|
groupRemoveStatus
|
||||||
|
} from '../actions/groups';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
@ -173,6 +177,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onGroupRemoveAccount(groupId, accountId) {
|
||||||
|
dispatch(createRemovedAccount(groupId, accountId));
|
||||||
|
},
|
||||||
|
|
||||||
|
onGroupRemoveStatus(groupId, statusId) {
|
||||||
|
dispatch(groupRemoveStatus(groupId, statusId));
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||||
|
@ -68,6 +68,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
anyMedia: PropTypes.bool,
|
anyMedia: PropTypes.bool,
|
||||||
shouldCondense: PropTypes.bool,
|
shouldCondense: PropTypes.bool,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
|
group: ImmutablePropTypes.map,
|
||||||
isModalOpen: PropTypes.bool,
|
isModalOpen: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,7 +120,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onSubmit(this.context.router ? this.context.router.history : null);
|
this.props.onSubmit(this.context.router ? this.context.router.history : null, this.props.group);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuggestionsClearRequested = () => {
|
onSuggestionsClearRequested = () => {
|
||||||
|
@ -34,8 +34,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(changeCompose(text));
|
dispatch(changeCompose(text));
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit (router) {
|
onSubmit (router, group) {
|
||||||
dispatch(submitCompose(router));
|
dispatch(submitCompose(router, group));
|
||||||
},
|
},
|
||||||
|
|
||||||
onClearSuggestions () {
|
onClearSuggestions () {
|
||||||
|
@ -1,76 +1,109 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
import { changeValue, submit, reset } from '../../../actions/group_editor';
|
||||||
import IconButton from '../../../components/icon_button';
|
import Icon from '../../../components/icon';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
label: { id: 'groups.new.title_placeholder', defaultMessage: 'New group title' },
|
title: { id: 'groups.form.title', defaultMessage: 'Enter a new group title' },
|
||||||
title: { id: 'groups.new.create', defaultMessage: 'Add group' },
|
description: { id: 'groups.form.description', defaultMessage: 'Enter the group description' },
|
||||||
|
coverImage: { id: 'groups.form.coverImage', defaultMessage: 'Upload a banner image' },
|
||||||
|
coverImageChange: { id: 'groups.form.coverImageChange', defaultMessage: 'Banner image selected' },
|
||||||
|
create: { id: 'groups.form.create', defaultMessage: 'Create group' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
value: state.getIn(['groupEditor', 'title']),
|
title: state.getIn(['group_editor', 'title']),
|
||||||
disabled: state.getIn(['groupEditor', 'isSubmitting']),
|
description: state.getIn(['group_editor', 'description']),
|
||||||
|
coverImage: state.getIn(['group_editor', 'coverImage']),
|
||||||
|
disabled: state.getIn(['group_editor', 'isSubmitting']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
onTitleChange: value => dispatch(changeValue('title', value)),
|
||||||
onSubmit: () => dispatch(submitListEditor(true)),
|
onDescriptionChange: value => dispatch(changeValue('description', value)),
|
||||||
|
onCoverImageChange: value => dispatch(changeValue('coverImage', value)),
|
||||||
|
onSubmit: routerHistory => dispatch(submit(routerHistory)),
|
||||||
|
reset: () => dispatch(reset()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class Create extends React.PureComponent {
|
class Create extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string.isRequired,
|
||||||
|
coverImage: PropTypes.object,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onTitleChange: PropTypes.func.isRequired,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChange = e => {
|
componentWillMount() {
|
||||||
this.props.onChange(e.target.value);
|
this.props.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTitleChange = e => {
|
||||||
|
this.props.onTitleChange(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDescriptionChange = e => {
|
||||||
|
this.props.onDescriptionChange(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCoverImageChange = e => {
|
||||||
|
this.props.onCoverImageChange(e.target.files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = e => {
|
handleSubmit = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onSubmit();
|
this.props.onSubmit(this.context.router.history);
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
this.props.onSubmit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { value, disabled, intl } = this.props;
|
const { title, description, coverImage, disabled, intl } = this.props;
|
||||||
|
|
||||||
const label = intl.formatMessage(messages.label);
|
|
||||||
const title = intl.formatMessage(messages.title);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
<form className='group-form' onSubmit={this.handleSubmit}>
|
||||||
<label>
|
<div>
|
||||||
<span style={{ display: 'none' }}>{label}</span>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className='setting-text'
|
className='group-form__input'
|
||||||
value={value}
|
value={title}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleTitleChange}
|
||||||
placeholder={label}
|
placeholder={intl.formatMessage(messages.title)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className='group-form__input'
|
||||||
|
value={description}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={this.handleDescriptionChange}
|
||||||
|
placeholder={intl.formatMessage(messages.description)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor='group_cover_image' className={classNames('group-form__file-label', { 'group-form__file-label--selected': coverImage !== null })}>
|
||||||
|
<Icon id='camera' /> {intl.formatMessage(coverImage === null ? messages.coverImage : messages.coverImageChange)}
|
||||||
</label>
|
</label>
|
||||||
|
<input
|
||||||
<IconButton
|
type='file'
|
||||||
|
className='group-form__file'
|
||||||
|
id='group_cover_image'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
icon='plus'
|
onChange={this.handleCoverImageChange}
|
||||||
title={title}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
/>
|
/>
|
||||||
|
<button className='standard-small'>{intl.formatMessage(messages.create)}</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
146
app/javascript/gabsocial/features/groups/edit/index.js
Normal file
146
app/javascript/gabsocial/features/groups/edit/index.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { changeValue, submit, setUp } from '../../../actions/group_editor';
|
||||||
|
import Icon from '../../../components/icon';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import LoadingIndicator from '../../../components/loading_indicator';
|
||||||
|
import Column from '../../../components/column';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'groups.form.title', defaultMessage: 'Title' },
|
||||||
|
description: { id: 'groups.form.description', defaultMessage: 'Description' },
|
||||||
|
coverImage: { id: 'groups.form.coverImage', defaultMessage: 'Upload new banner image (optional)' },
|
||||||
|
coverImageChange: { id: 'groups.form.coverImageChange', defaultMessage: 'Banner image selected' },
|
||||||
|
update: { id: 'groups.form.update', defaultMessage: 'Update group' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
group: state.getIn(['groups', props.params.id]),
|
||||||
|
title: state.getIn(['group_editor', 'title']),
|
||||||
|
description: state.getIn(['group_editor', 'description']),
|
||||||
|
coverImage: state.getIn(['group_editor', 'coverImage']),
|
||||||
|
disabled: state.getIn(['group_editor', 'isSubmitting']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onTitleChange: value => dispatch(changeValue('title', value)),
|
||||||
|
onDescriptionChange: value => dispatch(changeValue('description', value)),
|
||||||
|
onCoverImageChange: value => dispatch(changeValue('coverImage', value)),
|
||||||
|
onSubmit: routerHistory => dispatch(submit(routerHistory)),
|
||||||
|
setUp: group => dispatch(setUp(group)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Edit extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
group: ImmutablePropTypes.map,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string.isRequired,
|
||||||
|
coverImage: PropTypes.object,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onTitleChange: PropTypes.func.isRequired,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount(nextProps) {
|
||||||
|
if (this.props.group) {
|
||||||
|
this.props.setUp(this.props.group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (!this.props.group && nextProps.group) {
|
||||||
|
this.props.setUp(nextProps.group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTitleChange = e => {
|
||||||
|
this.props.onTitleChange(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDescriptionChange = e => {
|
||||||
|
this.props.onDescriptionChange(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCoverImageChange = e => {
|
||||||
|
this.props.onCoverImageChange(e.target.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onSubmit(this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.onSubmit(this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { group, title, description, coverImage, disabled, intl } = this.props;
|
||||||
|
|
||||||
|
if (typeof group === 'undefined') {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
} else if (group === false) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<MissingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className='group-form' onSubmit={this.handleSubmit}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className='group-form__input'
|
||||||
|
value={title}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={this.handleTitleChange}
|
||||||
|
placeholder={intl.formatMessage(messages.title)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className='group-form__input'
|
||||||
|
value={description}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={this.handleDescriptionChange}
|
||||||
|
placeholder={intl.formatMessage(messages.description)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor='group_cover_image' className={classNames('group-form__file-label', { 'group-form__file-label--selected': coverImage !== null })}>
|
||||||
|
<Icon id='camera' /> {intl.formatMessage(coverImage === null ? messages.coverImage : messages.coverImageChange)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
className='group-form__file'
|
||||||
|
id='group_cover_image'
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={this.handleCoverImageChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button>{intl.formatMessage(messages.update)}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
app/javascript/gabsocial/features/groups/index/card.js
Normal file
54
app/javascript/gabsocial/features/groups/index/card.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
members: { id: 'groups.card.members', defaultMessage: 'Members' },
|
||||||
|
view: { id: 'groups.card.view', defaultMessage: 'View' },
|
||||||
|
join: { id: 'groups.card.join', defaultMessage: 'Join' },
|
||||||
|
role_member: { id: 'groups.card.roles.member', defaultMessage: 'You\'re a member' },
|
||||||
|
role_admin: { id: 'groups.card.roles.admin', defaultMessage: 'You\'re an admin' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
group: state.getIn(['groups', id]),
|
||||||
|
relationships: state.getIn(['group_relationships', id]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class GroupCard extends ImmutablePureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
group: ImmutablePropTypes.map,
|
||||||
|
relationships: ImmutablePropTypes.map,
|
||||||
|
}
|
||||||
|
|
||||||
|
getRole() {
|
||||||
|
const { intl, relationships } = this.props;
|
||||||
|
|
||||||
|
if (!relationships) return null;
|
||||||
|
if (relationships.get('admin')) return intl.formatMessage(messages.role_admin);
|
||||||
|
if (relationships.get('member')) return intl.formatMessage(messages.role_member);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, group } = this.props;
|
||||||
|
const coverImageUrl = group.get('cover_image_url');
|
||||||
|
const role = this.getRole();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/groups/${group.get('id')}`} className="group-card">
|
||||||
|
<div className="group-card__header">{coverImageUrl && <img alt="" src={coverImageUrl} />}</div>
|
||||||
|
<div className="group-card__content">
|
||||||
|
<div className="group-card__title">{group.get('title')}</div>
|
||||||
|
<div className="group-card__meta"><strong>{shortNumberFormat(group.get('member_count'))}</strong> {intl.formatMessage(messages.members)}{role && <span> · {role}</span>}</div>
|
||||||
|
<div className="group-card__description">{group.get('description')}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,77 +2,88 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import LoadingIndicator from '../../../components/loading_indicator';
|
|
||||||
import Column from '../../ui/components/column';
|
|
||||||
import { fetchGroups } from '../../../actions/groups';
|
import { fetchGroups } from '../../../actions/groups';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import ColumnLink from '../../ui/components/column_link';
|
import { Link } from 'react-router-dom';
|
||||||
import ColumnSubheading from '../../ui/components/column_subheading';
|
import classNames from 'classnames';
|
||||||
import NewGroupForm from '../create';
|
import GroupCard from './card';
|
||||||
import { createSelector } from 'reselect';
|
import GroupCreate from '../create';
|
||||||
import ScrollableList from '../../../components/scrollable_list';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
||||||
subheading: { id: 'groups.subheading', defaultMessage: 'Your 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' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOrderedGroups = createSelector([state => state.get('groups')], groups => {
|
const mapStateToProps = (state, { activeTab }) => ({
|
||||||
if (!groups) {
|
groupIds: state.getIn(['group_lists', activeTab]),
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups.toList().filter(item => !!item);
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
groups: getOrderedGroups(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class Groups extends ImmutablePureComponent {
|
class Groups extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
|
activeTab: PropTypes.string.isRequired,
|
||||||
|
showCreateForm: PropTypes.bool,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
groups: ImmutablePropTypes.list,
|
groups: ImmutablePropTypes.map,
|
||||||
|
groupIds: ImmutablePropTypes.list,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
this.props.dispatch(fetchGroups());
|
this.props.dispatch(fetchGroups(this.props.activeTab));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(oldProps) {
|
||||||
|
if (this.props.activeTab && this.props.activeTab !== oldProps.activeTab) {
|
||||||
|
this.props.dispatch(fetchGroups(this.props.activeTab));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader() {
|
||||||
|
const { intl, activeTab } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group-column-header">
|
||||||
|
<div className="group-column-header__cta"><Link to="/groups/create" className="button standard-small">{intl.formatMessage(messages.create)}</Link></div>
|
||||||
|
<div className="group-column-header__title">{intl.formatMessage(messages.heading)}</div>
|
||||||
|
|
||||||
|
<div className="column-header__wrapper">
|
||||||
|
<h1 className="column-header">
|
||||||
|
<Link to='/groups' className={classNames('btn grouped', {'active': 'featured' === activeTab})}>
|
||||||
|
{intl.formatMessage(messages.tab_featured)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to='/groups/browse/member' className={classNames('btn grouped', {'active': 'member' === activeTab})}>
|
||||||
|
{intl.formatMessage(messages.tab_member)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to='/groups/browse/admin' className={classNames('btn grouped', {'active': 'admin' === activeTab})}>
|
||||||
|
{intl.formatMessage(messages.tab_admin)}
|
||||||
|
</Link>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, groups } = this.props;
|
const { groupIds, showCreateForm } = this.props;
|
||||||
|
|
||||||
if (!groups) {
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.groups' defaultMessage="No groups." />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column icon='list-ul' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
<div>
|
||||||
<NewGroupForm />
|
{!showCreateForm && this.renderHeader()}
|
||||||
|
{showCreateForm && <GroupCreate /> }
|
||||||
|
|
||||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
<div className="group-card-list">
|
||||||
<ScrollableList
|
{groupIds.map(id => <GroupCard key={id} id={id} />)}
|
||||||
scrollKey='lists'
|
</div>
|
||||||
emptyMessage={emptyMessage}
|
</div>
|
||||||
>
|
|
||||||
{groups.map(group =>
|
|
||||||
<ColumnLink key={group.get('id')} to={`/groups/${group.get('id')}`} icon='list-ul' text={group.get('title')} />
|
|
||||||
)}
|
|
||||||
</ScrollableList>
|
|
||||||
</Column>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
73
app/javascript/gabsocial/features/groups/members/index.js
Normal file
73
app/javascript/gabsocial/features/groups/members/index.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import LoadingIndicator from '../../../components/loading_indicator';
|
||||||
|
import {
|
||||||
|
fetchMembers,
|
||||||
|
expandMembers,
|
||||||
|
} from '../../../actions/groups';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import AccountContainer from '../../../containers/account_container';
|
||||||
|
import Column from '../../ui/components/column';
|
||||||
|
import ScrollableList from '../../../components/scrollable_list';
|
||||||
|
|
||||||
|
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']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class GroupMembers extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
params: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
accountIds: ImmutablePropTypes.list,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
const { params: { id } } = this.props;
|
||||||
|
|
||||||
|
this.props.dispatch(fetchMembers(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.params.id !== this.props.params.id) {
|
||||||
|
this.props.dispatch(fetchMembers(nextProps.params.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.dispatch(expandMembers(this.props.params.id));
|
||||||
|
}, 300, { leading: true });
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { accountIds, hasMore, group } = this.props;
|
||||||
|
|
||||||
|
if (!group || !accountIds) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import LoadingIndicator from '../../../components/loading_indicator';
|
||||||
|
import {
|
||||||
|
fetchRemovedAccounts,
|
||||||
|
expandRemovedAccounts,
|
||||||
|
removeRemovedAccount,
|
||||||
|
} from '../../../actions/groups';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import AccountContainer from '../../../containers/account_container';
|
||||||
|
import Column from '../../ui/components/column';
|
||||||
|
import ScrollableList from '../../../components/scrollable_list';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
remove: { id: 'groups.removed_accounts', defaultMessage: 'Allow joining' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { params: { id } }) => ({
|
||||||
|
group: state.getIn(['groups', id]),
|
||||||
|
accountIds: state.getIn(['user_lists', 'groups_removed_accounts', id, 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'groups_removed_accounts', id, 'next']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class GroupRemovedAccounts extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
params: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
accountIds: ImmutablePropTypes.list,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
const { params: { id } } = this.props;
|
||||||
|
|
||||||
|
this.props.dispatch(fetchRemovedAccounts(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.params.id !== this.props.params.id) {
|
||||||
|
this.props.dispatch(fetchRemovedAccounts(nextProps.params.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.dispatch(expandRemovedAccounts(this.props.params.id));
|
||||||
|
}, 300, { leading: true });
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { accountIds, hasMore, group, intl } = this.props;
|
||||||
|
|
||||||
|
if (!group || !accountIds) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='removed_accounts'
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
emptyMessage={<FormattedMessage id='group.removed_accounts.empty' defaultMessage='This group does not has any removed accounts.' />}
|
||||||
|
>
|
||||||
|
{accountIds.map(id => <AccountContainer
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
actionIcon="remove"
|
||||||
|
onActionClick={() => this.props.dispatch(removeRemovedAccount(group.get('id'), id))}
|
||||||
|
actionTitle={intl.formatMessage(messages.remove)}
|
||||||
|
/>)}
|
||||||
|
</ScrollableList>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Item from './item';
|
||||||
|
import Icon from 'gabsocial/components/icon';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'groups.sidebar-panel.title', defaultMessage: 'Groups you\'re in' },
|
||||||
|
show_all: { id: 'groups.sidebar-panel.show_all', defaultMessage: 'Show all' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
groupIds: state.getIn(['group_lists', 'member']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class GroupSidebarPanel extends ImmutablePureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
groupIds: ImmutablePropTypes.list,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, groupIds } = this.props;
|
||||||
|
const count = groupIds.count();
|
||||||
|
|
||||||
|
// Only when there are groups to show
|
||||||
|
if (count === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='wtf-panel group-sidebar-panel'>
|
||||||
|
<div className='wtf-panel-header'>
|
||||||
|
<Icon id='users' className='wtf-panel-header__icon' />
|
||||||
|
<span className='wtf-panel-header__label'>{intl.formatMessage(messages.title)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='wtf-panel__content'>
|
||||||
|
<div className="group-sidebar-panel__items">
|
||||||
|
{groupIds.slice(0, 10).map(groupId => <Item key={groupId} id={groupId} />)}
|
||||||
|
{count > 10 && <Link className="group-sidebar-panel__items__show-all" to='/groups/browse/member'>{intl.formatMessage(messages.show_all)}</Link>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
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';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
new_statuses: { id: 'groups.sidebar-panel.item.view', defaultMessage: 'new gabs' },
|
||||||
|
no_recent_activity: { id: 'groups.sidebar-panel.item.no_recent_activity', defaultMessage: 'No recent activity' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
group: state.getIn(['groups', id]),
|
||||||
|
relationships: state.getIn(['group_relationships', id]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Item extends ImmutablePureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
group: ImmutablePropTypes.map,
|
||||||
|
relationships: ImmutablePropTypes.map,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, group, relationships } = this.props;
|
||||||
|
|
||||||
|
// Wait for relationships
|
||||||
|
if (!relationships) return null;
|
||||||
|
|
||||||
|
const unreadCount = relationships.get('unread_count');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/groups/${group.get('id')}`} className="group-sidebar-panel__item">
|
||||||
|
<div className="group-sidebar-panel__item__title">{group.get('title')}</div>
|
||||||
|
<div className="group-sidebar-panel__item__meta">
|
||||||
|
{unreadCount > 0 && <span className="group-sidebar-panel__item__meta__unread">{shortNumberFormat(unreadCount)} {intl.formatMessage(messages.new_statuses)}</span>}
|
||||||
|
{unreadCount === 0 && <span>{intl.formatMessage(messages.no_recent_activity)}</span>}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import InnerHeader from './inner_header';
|
import Button from 'gabsocial/components/button';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import DropdownMenuContainer from '../../../../containers/dropdown_menu_container';
|
||||||
|
|
||||||
export default class Header extends ImmutablePureComponent {
|
const messages = defineMessages({
|
||||||
|
join: { id: 'groups.join', defaultMessage: 'Join group' },
|
||||||
|
leave: { id: 'groups.leave', defaultMessage: 'Leave group' },
|
||||||
|
removed_accounts: { id: 'groups.removed_accounts', defaultMessage: 'Removed Accounts' },
|
||||||
|
edit: { id: 'groups.edit', defaultMessage: 'Edit' }
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class Header extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
group: ImmutablePropTypes.map,
|
group: ImmutablePropTypes.map,
|
||||||
relationships: ImmutablePropTypes.map,
|
relationships: ImmutablePropTypes.map,
|
||||||
@ -18,27 +26,52 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
getActionButton() {
|
||||||
const { group, relationships, toggleMembership } = this.props;
|
const { group, relationships, toggleMembership, intl } = this.props;
|
||||||
|
const toggle = () => toggleMembership(group, relationships);
|
||||||
|
|
||||||
if (group === null) {
|
if (!relationships) {
|
||||||
|
return '';
|
||||||
|
} else if (!relationships.get('member')) {
|
||||||
|
return <Button className='logo-button' text={intl.formatMessage(messages.join)} onClick={toggle} />;
|
||||||
|
} else if (relationships.get('member')) {
|
||||||
|
return <Button className='logo-button' text={intl.formatMessage(messages.leave, { name: group.get('title') })} onClick={toggle} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAdminMenu() {
|
||||||
|
const { group, intl } = this.props;
|
||||||
|
|
||||||
|
const menu = [
|
||||||
|
{ text: intl.formatMessage(messages.edit), to: `/groups/${group.get('id')}/edit` },
|
||||||
|
{ text: intl.formatMessage(messages.removed_accounts), to: `/groups/${group.get('id')}/removed_accounts` },
|
||||||
|
];
|
||||||
|
|
||||||
|
return <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { group, relationships } = this.props;
|
||||||
|
|
||||||
|
if (!group || !relationships) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-timeline__header'>
|
<div className='group__header-container'>
|
||||||
<InnerHeader
|
<div className="group__header">
|
||||||
group={group}
|
<div className='group__cover'>
|
||||||
relationships={relationships}
|
<img src={group.get('cover_image_url')} alt='' className='parallax' />
|
||||||
toggleMembership={toggleMembership}
|
</div>
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='account__section-headline'>
|
<div className='group__tabs'>
|
||||||
<NavLink exact to={`/groups/${group.get('id')}`}><FormattedMessage id='groups.posts' defaultMessage='Posts' /></NavLink>
|
<NavLink exact className='group__tabs__tab' activeClassName='group__tabs__tab--active' to={`/groups/${group.get('id')}`}>Posts</NavLink>
|
||||||
<NavLink exact to={`/groups/${group.get('id')}/accounts`}><FormattedMessage id='group.accounts' defaultMessage='Members' /></NavLink>
|
<NavLink exact className='group__tabs__tab' activeClassName='group__tabs__tab--active' to={`/groups/${group.get('id')}/members`}>Members</NavLink>
|
||||||
|
{this.getActionButton()}
|
||||||
|
{relationships.get('admin') && this.getAdminMenu()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import Button from 'gabsocial/components/button';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import Icon from 'gabsocial/components/icon';
|
|
||||||
import DropdownMenuContainer from 'gabsocial/containers/dropdown_menu_container';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
join: { id: 'groups.join', defaultMessage: 'Join' },
|
|
||||||
leave: { id: 'groups.leave', defaultMessage: 'Leave' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
class InnerHeader extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
group: ImmutablePropTypes.map,
|
|
||||||
relationships: ImmutablePropTypes.map,
|
|
||||||
toggleMembership: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
isStatusesPageActive = (match, location) => {
|
|
||||||
if (!match) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !location.pathname.match(/\/(accounts)\/?$/);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { group, relationships, intl } = this.props;
|
|
||||||
|
|
||||||
if (!group || !relationships) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = [];
|
|
||||||
let actionBtn = '';
|
|
||||||
let lockedIcon = '';
|
|
||||||
let menu = [];
|
|
||||||
|
|
||||||
if (relationships.get('admin')) {
|
|
||||||
info.push(<span key='admin'><FormattedMessage id='group.admin' defaultMessage='You are an admin' /></span>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!relationships) { // Wait until the relationship is loaded
|
|
||||||
actionBtn = '';
|
|
||||||
} else if (!relationships.get('member')) {
|
|
||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.join)} onClick={() => this.props.toggleMembership(group, relationships)} />;
|
|
||||||
} else if (relationships.get('member')) {
|
|
||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.leave, { name: group.get('title') })} onClick={() => this.props.toggleMembership(group, relationships)} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.get('archived')) {
|
|
||||||
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.group_archived)} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='account__header'>
|
|
||||||
<div className='account__header__image'>
|
|
||||||
<div className='account__header__info'>
|
|
||||||
<img src={group.get('cover_image_url')} alt='' className='parallax' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='account__header__bar'>
|
|
||||||
<div className='account__header__tabs'>
|
|
||||||
<div className='account__header__tabs__buttons'>
|
|
||||||
{actionBtn}
|
|
||||||
|
|
||||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='account__header__tabs__name'>
|
|
||||||
<h1>
|
|
||||||
<span>{group.get('title')} {info}</span>
|
|
||||||
<small>{lockedIcon}</small>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='account__header__extra'>
|
|
||||||
<div className='account__header__bio'>
|
|
||||||
{group.get('description').length > 0 && <div className='account__header__content'>{group.get('description')}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
group_archived: { id: 'group.detail.archived_group', defaultMessage: 'Archived group' },
|
||||||
|
group_admin: { id: 'groups.detail.role_admin', defaultMessage: 'You\'re an admin' }
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class GroupPanel extends ImmutablePureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
group: ImmutablePropTypes.map,
|
||||||
|
relationships: ImmutablePropTypes.map,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { group, relationships, intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group__panel">
|
||||||
|
<h1 className="group__panel__title">
|
||||||
|
{group.get('title')}
|
||||||
|
{group.get('archived') && <Icon id='lock' title={intl.formatMessage(messages.group_archived)} />}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{relationships.get('admin') && <span className="group__panel__label">{intl.formatMessage(messages.group_admin)}</span>}
|
||||||
|
|
||||||
|
<div className="group__panel__description">{group.get('description')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,24 +4,25 @@ import PropTypes from 'prop-types';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||||
import Column from '../../../components/column';
|
import Column from '../../../components/column';
|
||||||
import ColumnHeader from '../../../components/column_header';
|
|
||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
import { connectGroupStream } from '../../../actions/streaming';
|
import { connectGroupStream } from '../../../actions/streaming';
|
||||||
import { expandGroupTimeline } from '../../../actions/timelines';
|
import { expandGroupTimeline } from '../../../actions/timelines';
|
||||||
import { fetchGroup } from '../../../actions/groups';
|
|
||||||
import MissingIndicator from '../../../components/missing_indicator';
|
import MissingIndicator from '../../../components/missing_indicator';
|
||||||
import LoadingIndicator from '../../../components/loading_indicator';
|
import LoadingIndicator from '../../../components/loading_indicator';
|
||||||
import HeaderContainer from './containers/header_container';
|
import ComposeFormContainer from '../../../../gabsocial/features/compose/containers/compose_form_container';
|
||||||
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
import Avatar from '../../../components/avatar';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
|
account: state.getIn(['accounts', me]),
|
||||||
group: state.getIn(['groups', props.params.id]),
|
group: state.getIn(['groups', props.params.id]),
|
||||||
|
relationships: state.getIn(['group_relationships', props.params.id]),
|
||||||
hasUnread: state.getIn(['timelines', `group:${props.params.id}`, 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', `group:${props.params.id}`, 'unread']) > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class GroupTimeline extends React.PureComponent {
|
class GroupTimeline extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
};
|
};
|
||||||
@ -29,8 +30,11 @@ class GroupTimeline extends React.PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
columnId: PropTypes.string,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||||
|
relationships: ImmutablePropTypes.map,
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,7 +42,6 @@ class GroupTimeline extends React.PureComponent {
|
|||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
|
|
||||||
dispatch(fetchGroup(id));
|
|
||||||
dispatch(expandGroupTimeline(id));
|
dispatch(expandGroupTimeline(id));
|
||||||
|
|
||||||
this.disconnect = dispatch(connectGroupStream(id));
|
this.disconnect = dispatch(connectGroupStream(id));
|
||||||
@ -57,16 +60,13 @@ class GroupTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hasUnread, group } = this.props;
|
const { columnId, group, relationships, account } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
const title = group ? group.get('title') : id;
|
|
||||||
|
|
||||||
if (typeof group === 'undefined') {
|
if (typeof group === 'undefined' || !relationships) {
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<div>
|
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
</div>
|
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
} else if (group === false) {
|
} else if (group === false) {
|
||||||
@ -78,25 +78,27 @@ class GroupTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={title}>
|
<div>
|
||||||
<ColumnHeader icon='list-ul' active={hasUnread} title={title}>
|
{relationships.get('member') && (
|
||||||
<div className='column-header__links'>
|
<div className='timeline-compose-block'>
|
||||||
{/* Leave might be here */}
|
<div className='timeline-compose-block__avatar'>
|
||||||
|
<Avatar account={account} size={46} />
|
||||||
</div>
|
</div>
|
||||||
|
<ComposeFormContainer group={group} shouldCondense={true} autoFocus={false}/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<hr />
|
<div className='group__feed'>
|
||||||
</ColumnHeader>
|
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
prepend={<HeaderContainer groupId={id} />}
|
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
scrollKey='group_timeline'
|
scrollKey={`group_timeline-${columnId}`}
|
||||||
timelineId={`group:${id}`}
|
timelineId={`group:${id}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
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.' />}
|
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.' />}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,9 @@ export const privateLinks = [
|
|||||||
<NotificationsCounterIcon />
|
<NotificationsCounterIcon />
|
||||||
<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />
|
<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />
|
||||||
</NavLink>,
|
</NavLink>,
|
||||||
// <NavLink className='tabs-bar__link groups' to='/groups' data-preview-title-id='column.groups' >
|
<NavLink className='tabs-bar__link groups' to='/groups' data-preview-title-id='column.groups' >
|
||||||
// <FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />
|
<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />
|
||||||
// </NavLink>,
|
</NavLink>,
|
||||||
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' >
|
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' >
|
||||||
<FormattedMessage id='tabs_bar.search' defaultMessage='Search' />
|
<FormattedMessage id='tabs_bar.search' defaultMessage='Search' />
|
||||||
</NavLink>,
|
</NavLink>,
|
||||||
|
@ -25,8 +25,11 @@ import TabsBar from './components/tabs_bar';
|
|||||||
import WhoToFollowPanel from './components/who_to_follow_panel';
|
import WhoToFollowPanel from './components/who_to_follow_panel';
|
||||||
import LinkFooter from './components/link_footer';
|
import LinkFooter from './components/link_footer';
|
||||||
import ProfilePage from 'gabsocial/pages/profile_page';
|
import ProfilePage from 'gabsocial/pages/profile_page';
|
||||||
|
import GroupsPage from 'gabsocial/pages/groups_page';
|
||||||
|
import GroupPage from 'gabsocial/pages/group_page';
|
||||||
import SearchPage from 'gabsocial/pages/search_page';
|
import SearchPage from 'gabsocial/pages/search_page';
|
||||||
import HomePage from 'gabsocial/pages/home_page';
|
import HomePage from 'gabsocial/pages/home_page';
|
||||||
|
import GroupSidebarPanel from '../groups/sidebar_panel';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Status,
|
Status,
|
||||||
@ -55,6 +58,10 @@ import {
|
|||||||
GroupTimeline,
|
GroupTimeline,
|
||||||
ListTimeline,
|
ListTimeline,
|
||||||
Lists,
|
Lists,
|
||||||
|
GroupMembers,
|
||||||
|
GroupRemovedAccounts,
|
||||||
|
GroupCreate,
|
||||||
|
GroupEdit,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { me, meUsername } from '../../initial_state';
|
import { me, meUsername } from '../../initial_state';
|
||||||
import { previewState as previewMediaState } from './components/media_modal';
|
import { previewState as previewMediaState } from './components/media_modal';
|
||||||
@ -63,6 +70,7 @@ import { previewState as previewVideoState } from './components/video_modal';
|
|||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
import '../../components/status';
|
import '../../components/status';
|
||||||
|
import { fetchGroups } from '../../actions/groups';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Gab Social.' },
|
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Gab Social.' },
|
||||||
@ -116,12 +124,14 @@ const LAYOUT = {
|
|||||||
],
|
],
|
||||||
RIGHT: [
|
RIGHT: [
|
||||||
// <TrendsPanel />,
|
// <TrendsPanel />,
|
||||||
|
<GroupSidebarPanel />
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
STATUS: {
|
STATUS: {
|
||||||
TOP: null,
|
TOP: null,
|
||||||
LEFT: null,
|
LEFT: null,
|
||||||
RIGHT: [
|
RIGHT: [
|
||||||
|
<GroupSidebarPanel />,
|
||||||
<WhoToFollowPanel key='0' />,
|
<WhoToFollowPanel key='0' />,
|
||||||
// <TrendsPanel />,
|
// <TrendsPanel />,
|
||||||
<LinkFooter key='1' />,
|
<LinkFooter key='1' />,
|
||||||
@ -174,8 +184,14 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||||||
<WrappedRoute path='/home' exact page={HomePage} component={HomeTimeline} content={children} />
|
<WrappedRoute path='/home' exact page={HomePage} component={HomeTimeline} content={children} />
|
||||||
<WrappedRoute path='/timeline/all' exact page={HomePage} component={CommunityTimeline} content={children} />
|
<WrappedRoute path='/timeline/all' exact page={HomePage} component={CommunityTimeline} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/groups' component={Groups} content={children} />
|
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'featured' }} />
|
||||||
<WrappedRoute path='/groups/:id' component={GroupTimeline} content={children} />
|
<WrappedRoute path='/groups/create' page={GroupsPage} component={Groups} content={children} componentParams={{ showCreateForm: true, activeTab: 'featured' }} />
|
||||||
|
<WrappedRoute path='/groups/browse/member' page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'member' }} />
|
||||||
|
<WrappedRoute path='/groups/browse/admin' page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'admin' }} />
|
||||||
|
<WrappedRoute path='/groups/:id/members' page={GroupPage} component={GroupMembers} content={children} />
|
||||||
|
<WrappedRoute path='/groups/:id/removed_accounts' page={GroupPage} component={GroupRemovedAccounts} content={children} />
|
||||||
|
<WrappedRoute path='/groups/:id/edit' page={GroupPage} component={GroupEdit} content={children} />
|
||||||
|
<WrappedRoute path='/groups/:id' page={GroupPage} component={GroupTimeline} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
|
|
||||||
@ -360,6 +376,7 @@ class UI extends React.PureComponent {
|
|||||||
if (me) {
|
if (me) {
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
|
this.props.dispatch(fetchGroups('member'));
|
||||||
|
|
||||||
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,22 @@ export function GroupTimeline () {
|
|||||||
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/timeline');
|
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GroupMembers () {
|
||||||
|
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/members');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupRemovedAccounts () {
|
||||||
|
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/removed_accounts');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupCreate () {
|
||||||
|
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/create');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupEdit () {
|
||||||
|
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/edit');
|
||||||
|
}
|
||||||
|
|
||||||
export function Groups () {
|
export function Groups () {
|
||||||
return import(/* webpackChunkName: "features/groups/index" */'../../groups/index');
|
return import(/* webpackChunkName: "features/groups/index" */'../../groups/index');
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@
|
|||||||
"column.lists": "Lists",
|
"column.lists": "Lists",
|
||||||
"column.mutes": "Muted users",
|
"column.mutes": "Muted users",
|
||||||
"column.notifications": "Notifications",
|
"column.notifications": "Notifications",
|
||||||
|
"column.groups": "Groups",
|
||||||
"column.pins": "Pinned gabs",
|
"column.pins": "Pinned gabs",
|
||||||
"column.public": "Federated timeline",
|
"column.public": "Federated timeline",
|
||||||
"column_back_button.label": "Back",
|
"column_back_button.label": "Back",
|
||||||
|
73
app/javascript/gabsocial/pages/group_page.js
Normal file
73
app/javascript/gabsocial/pages/group_page.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { PropTypes } from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import WhoToFollowPanel from '../features/ui/components/who_to_follow_panel';
|
||||||
|
import LinkFooter from '../features/ui/components/link_footer';
|
||||||
|
import PromoPanel from '../features/ui/components/promo_panel';
|
||||||
|
import HeaderContainer from '../features/groups/timeline/containers/header_container';
|
||||||
|
import GroupPanel from '../features/groups/timeline/components/panel';
|
||||||
|
import { fetchGroup } from '../actions/groups';
|
||||||
|
import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { params: { id } }) => ({
|
||||||
|
group: state.getIn(['groups', id]),
|
||||||
|
relationships: state.getIn(['group_relationships', id]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class GroupPage extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
group: ImmutablePropTypes.map,
|
||||||
|
relationships: ImmutablePropTypes.map,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const { params: { id }, dispatch } = this.props;
|
||||||
|
|
||||||
|
dispatch(fetchGroup(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { children, group, relationships } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='page group'>
|
||||||
|
{group && <HeaderContainer groupId={group.get('id')} />}
|
||||||
|
|
||||||
|
<div className='page__columns'>
|
||||||
|
<div className='columns-area__panels'>
|
||||||
|
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
|
||||||
|
<div className='columns-area__panels__pane__inner'>
|
||||||
|
{group && relationships &&
|
||||||
|
<GroupPanel
|
||||||
|
group={group}
|
||||||
|
relationships={relationships}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
<PromoPanel />
|
||||||
|
<LinkFooter />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='columns-area__panels__main'>
|
||||||
|
<div className='columns-area columns-area--mobile'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
|
||||||
|
<div className='columns-area__panels__pane__inner'>
|
||||||
|
<GroupSidebarPanel />
|
||||||
|
<WhoToFollowPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
55
app/javascript/gabsocial/pages/groups_page.js
Normal file
55
app/javascript/gabsocial/pages/groups_page.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import WhoToFollowPanel from '../features/ui/components/who_to_follow_panel';
|
||||||
|
import LinkFooter from '../features/ui/components/link_footer';
|
||||||
|
import PromoPanel from '../features/ui/components/promo_panel';
|
||||||
|
import UserPanel from '../features/ui/components/user_panel';
|
||||||
|
import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
account: state.getIn(['accounts', me]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class GroupsPage extends ImmutablePureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='page'>
|
||||||
|
<div className='page__columns'>
|
||||||
|
<div className='columns-area__panels'>
|
||||||
|
|
||||||
|
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
|
||||||
|
<div className='columns-area__panels__pane__inner'>
|
||||||
|
<UserPanel />
|
||||||
|
<PromoPanel />
|
||||||
|
<LinkFooter />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='columns-area__panels__main'>
|
||||||
|
<div className='columns-area columns-area--mobile'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
|
||||||
|
<div className='columns-area__panels__pane__inner'>
|
||||||
|
<WhoToFollowPanel />
|
||||||
|
<GroupSidebarPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import PromoPanel from '../features/ui/components/promo_panel';
|
|||||||
import UserPanel from '../features/ui/components/user_panel';
|
import UserPanel from '../features/ui/components/user_panel';
|
||||||
import ComposeFormContainer from '../features/compose/containers/compose_form_container';
|
import ComposeFormContainer from '../features/compose/containers/compose_form_container';
|
||||||
import Avatar from '../components/avatar';
|
import Avatar from '../components/avatar';
|
||||||
|
import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
account: state.getIn(['accounts', me]),
|
account: state.getIn(['accounts', me]),
|
||||||
@ -48,6 +49,7 @@ class HomePage extends ImmutablePureComponent {
|
|||||||
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
|
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
|
||||||
<div className='columns-area__panels__pane__inner'>
|
<div className='columns-area__panels__pane__inner'>
|
||||||
<WhoToFollowPanel />
|
<WhoToFollowPanel />
|
||||||
|
<GroupSidebarPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
57
app/javascript/gabsocial/reducers/group_editor.js
Normal file
57
app/javascript/gabsocial/reducers/group_editor.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
import {
|
||||||
|
GROUP_CREATE_REQUEST,
|
||||||
|
GROUP_CREATE_FAIL,
|
||||||
|
GROUP_CREATE_SUCCESS,
|
||||||
|
GROUP_UPDATE_REQUEST,
|
||||||
|
GROUP_UPDATE_FAIL,
|
||||||
|
GROUP_UPDATE_SUCCESS,
|
||||||
|
GROUP_EDITOR_RESET,
|
||||||
|
GROUP_EDITOR_SETUP,
|
||||||
|
GROUP_EDITOR_VALUE_CHANGE,
|
||||||
|
} from '../actions/group_editor';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
groupId: null,
|
||||||
|
isSubmitting: false,
|
||||||
|
isChanged: false,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
coverImage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function groupEditorReducer(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case GROUP_EDITOR_RESET:
|
||||||
|
return initialState;
|
||||||
|
case GROUP_EDITOR_SETUP:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('groupId', action.group.get('id'));
|
||||||
|
map.set('title', action.group.get('title'));
|
||||||
|
map.set('description', action.group.get('description'));
|
||||||
|
map.set('isSubmitting', false);
|
||||||
|
});
|
||||||
|
case GROUP_EDITOR_VALUE_CHANGE:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set(action.field, action.value);
|
||||||
|
map.set('isChanged', true);
|
||||||
|
});
|
||||||
|
case GROUP_CREATE_REQUEST:
|
||||||
|
case GROUP_UPDATE_REQUEST:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('isSubmitting', true);
|
||||||
|
map.set('isChanged', false);
|
||||||
|
});
|
||||||
|
case GROUP_CREATE_FAIL:
|
||||||
|
case GROUP_UPDATE_FAIL:
|
||||||
|
return state.set('isSubmitting', false);
|
||||||
|
case GROUP_CREATE_SUCCESS:
|
||||||
|
case GROUP_UPDATE_SUCCESS:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('isSubmitting', false);
|
||||||
|
map.set('groupId', action.group.id);
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
21
app/javascript/gabsocial/reducers/group_lists.js
Normal file
21
app/javascript/gabsocial/reducers/group_lists.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
import { GROUPS_FETCH_SUCCESS } from '../actions/groups';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
featured: ImmutableList(),
|
||||||
|
member: ImmutableList(),
|
||||||
|
admin: ImmutableList(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeList = (state, type, id, groups) => {
|
||||||
|
return state.set(type, ImmutableList(groups.map(item => item.id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function groupLists(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case GROUPS_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, action.tab, action.id, action.groups);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
@ -3,6 +3,7 @@ import {
|
|||||||
GROUP_FETCH_FAIL,
|
GROUP_FETCH_FAIL,
|
||||||
GROUPS_FETCH_SUCCESS,
|
GROUPS_FETCH_SUCCESS,
|
||||||
} from '../actions/groups';
|
} from '../actions/groups';
|
||||||
|
import { GROUP_UPDATE_SUCCESS } from '../actions/group_editor';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
@ -20,6 +21,7 @@ const normalizeGroups = (state, groups) => {
|
|||||||
export default function groups(state = initialState, action) {
|
export default function groups(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case GROUP_FETCH_SUCCESS:
|
case GROUP_FETCH_SUCCESS:
|
||||||
|
case GROUP_UPDATE_SUCCESS:
|
||||||
return normalizeGroup(state, action.group);
|
return normalizeGroup(state, action.group);
|
||||||
case GROUPS_FETCH_SUCCESS:
|
case GROUPS_FETCH_SUCCESS:
|
||||||
return normalizeGroups(state, action.groups);
|
return normalizeGroups(state, action.groups);
|
||||||
|
@ -34,6 +34,8 @@ import identity_proofs from './identity_proofs';
|
|||||||
import trends from './trends';
|
import trends from './trends';
|
||||||
import groups from './groups';
|
import groups from './groups';
|
||||||
import group_relationships from './group_relationships';
|
import group_relationships from './group_relationships';
|
||||||
|
import group_lists from './group_lists';
|
||||||
|
import group_editor from './group_editor';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
dropdown_menu,
|
dropdown_menu,
|
||||||
@ -71,6 +73,8 @@ const reducers = {
|
|||||||
trends,
|
trends,
|
||||||
groups,
|
groups,
|
||||||
group_relationships,
|
group_relationships,
|
||||||
|
group_lists,
|
||||||
|
group_editor,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
export default combineReducers(reducers);
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
import compareId from '../compare_id';
|
import compareId from '../compare_id';
|
||||||
|
import { GROUP_REMOVE_STATUS_SUCCESS } from '../actions/groups';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
@ -151,6 +152,10 @@ const filterTimeline = (timeline, state, relationship, statuses) =>
|
|||||||
statuses.getIn([statusId, 'account']) === relationship.id
|
statuses.getIn([statusId, 'account']) === relationship.id
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const removeStatusFromGroup = (state, groupId, statusId) => {
|
||||||
|
return state.updateIn([`group:${groupId}`, 'items'], list => list.filterNot(item => item === statusId));
|
||||||
|
};
|
||||||
|
|
||||||
export default function timelines(state = initialState, action) {
|
export default function timelines(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case TIMELINE_EXPAND_REQUEST:
|
case TIMELINE_EXPAND_REQUEST:
|
||||||
@ -187,6 +192,8 @@ export default function timelines(state = initialState, action) {
|
|||||||
initialTimeline,
|
initialTimeline,
|
||||||
map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
|
map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
|
||||||
);
|
);
|
||||||
|
case GROUP_REMOVE_STATUS_SUCCESS:
|
||||||
|
return removeStatusFromGroup(state, action.groupId, action.id)
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,13 @@ import {
|
|||||||
MUTES_EXPAND_SUCCESS,
|
MUTES_EXPAND_SUCCESS,
|
||||||
} from '../actions/mutes';
|
} from '../actions/mutes';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
import {
|
||||||
|
GROUP_MEMBERS_FETCH_SUCCESS,
|
||||||
|
GROUP_MEMBERS_EXPAND_SUCCESS,
|
||||||
|
GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
|
||||||
|
GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
|
||||||
|
GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
|
||||||
|
} from '../actions/groups';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
followers: ImmutableMap(),
|
followers: ImmutableMap(),
|
||||||
@ -30,6 +37,8 @@ const initialState = ImmutableMap({
|
|||||||
follow_requests: ImmutableMap(),
|
follow_requests: ImmutableMap(),
|
||||||
blocks: ImmutableMap(),
|
blocks: ImmutableMap(),
|
||||||
mutes: ImmutableMap(),
|
mutes: ImmutableMap(),
|
||||||
|
groups: ImmutableMap(),
|
||||||
|
groups_removed_accounts: ImmutableMap(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeList = (state, type, id, accounts, next) => {
|
const normalizeList = (state, type, id, accounts, next) => {
|
||||||
@ -74,6 +83,16 @@ export default function userLists(state = initialState, action) {
|
|||||||
return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||||
case MUTES_EXPAND_SUCCESS:
|
case MUTES_EXPAND_SUCCESS:
|
||||||
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||||
|
case GROUP_MEMBERS_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, 'groups', action.id, action.accounts, action.next);
|
||||||
|
case GROUP_MEMBERS_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, 'groups', action.id, action.accounts, action.next);
|
||||||
|
case GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, 'groups_removed_accounts', action.id, action.accounts, action.next);
|
||||||
|
case GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, 'groups_removed_accounts', action.id, action.accounts, action.next);
|
||||||
|
case GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS:
|
||||||
|
return state.updateIn(['groups_removed_accounts', action.groupId, 'items'], list => list.filterNot(item => item === action.id));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,17 @@
|
|||||||
@import 'gabsocial/components';
|
@import 'gabsocial/components';
|
||||||
|
|
||||||
// COMPONENTS
|
// COMPONENTS
|
||||||
|
@import 'gabsocial/components/buttons';
|
||||||
@import 'gabsocial/components/tabs-bar';
|
@import 'gabsocial/components/tabs-bar';
|
||||||
@import 'gabsocial/components/dropdown-menu';
|
@import 'gabsocial/components/dropdown-menu';
|
||||||
@import 'gabsocial/components/modal';
|
@import 'gabsocial/components/modal';
|
||||||
@import 'gabsocial/components/account-header';
|
@import 'gabsocial/components/account-header';
|
||||||
@import 'gabsocial/components/user-panel';
|
@import 'gabsocial/components/user-panel';
|
||||||
@import 'gabsocial/components/compose-form';
|
@import 'gabsocial/components/compose-form';
|
||||||
|
@import 'gabsocial/components/group-card';
|
||||||
|
@import 'gabsocial/components/group-detail';
|
||||||
|
@import 'gabsocial/components/group-form';
|
||||||
|
@import 'gabsocial/components/group-sidebar-panel';
|
||||||
|
|
||||||
@import 'gabsocial/polls';
|
@import 'gabsocial/polls';
|
||||||
@import 'gabsocial/introduction';
|
@import 'gabsocial/introduction';
|
||||||
|
@ -1,3 +1,23 @@
|
|||||||
|
// NEW GAB SPECIFIC MIXINS
|
||||||
|
|
||||||
|
// THEME MIXINS
|
||||||
|
|
||||||
|
// standard container drop shadow
|
||||||
|
@mixin light-theme-shadow() {box-shadow: 0 0 6px 0 rgba(0,0,0,0.1);}
|
||||||
|
|
||||||
|
// common properties for all standard containers
|
||||||
|
@mixin gab-container-standards() {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: $gab-background-container;
|
||||||
|
body.theme-gabsocial-light & {
|
||||||
|
@include light-theme-shadow();
|
||||||
|
background: $gab-background-container-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// OLDER MIXINS
|
||||||
@mixin avatar-radius() {
|
@mixin avatar-radius() {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: transparent no-repeat;
|
background: transparent no-repeat;
|
||||||
|
@ -1303,7 +1303,7 @@ a.account__display-name {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
overflow-x: auto;
|
//overflow-x: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&__panels {
|
&__panels {
|
||||||
@ -1321,14 +1321,14 @@ a.account__display-name {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding-top: 10px;
|
padding-top: 15px;
|
||||||
|
|
||||||
&--start {
|
&--start {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__inner {
|
&__inner {
|
||||||
width: 285px;
|
width: 265px;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -1342,7 +1342,8 @@ a.account__display-name {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@media screen and (min-width: 360px) {
|
@media screen and (min-width: 360px) {
|
||||||
padding: 0 10px;
|
margin: 0 20px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1501,7 +1502,7 @@ a.account__display-name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 360px) {
|
@media screen and (min-width: 360px) {
|
||||||
padding: 10px 0;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 630px) {
|
@media screen and (min-width: 630px) {
|
||||||
|
23
app/javascript/styles/gabsocial/components/buttons.scss
Normal file
23
app/javascript/styles/gabsocial/components/buttons.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
button,
|
||||||
|
a.button {
|
||||||
|
&.standard {
|
||||||
|
|
||||||
|
// NOTE - will define the larger standard buttons here and apply class where used.
|
||||||
|
|
||||||
|
&-small {
|
||||||
|
height: 20px;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
@include font-size(11);
|
||||||
|
@include line-height(11);
|
||||||
|
@include font-weight(bold);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: white;
|
||||||
|
background: $gab-small-cta-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
72
app/javascript/styles/gabsocial/components/group-card.scss
Normal file
72
app/javascript/styles/gabsocial/components/group-card.scss
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
.group-column-header {
|
||||||
|
overflow: hidden;
|
||||||
|
@include gab-container-standards();
|
||||||
|
.group-column-header__title {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.group-column-header__cta {
|
||||||
|
float: right;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 17px;
|
||||||
|
a {color: #fff;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-card {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 calc(50% - 20px/2);
|
||||||
|
@include gab-container-standards();
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.group-card__header {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
background: $gab-background-container;;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-card__content {
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.group-card__title {
|
||||||
|
color: $primary-text-color;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-card__meta {
|
||||||
|
color: $gab-secondary-text;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-card__description {
|
||||||
|
color: $primary-text-color;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.group-card__title {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
app/javascript/styles/gabsocial/components/group-detail.scss
Normal file
84
app/javascript/styles/gabsocial/components/group-detail.scss
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
.group {
|
||||||
|
.group__header-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group__header {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1150px;
|
||||||
|
background: $gab-background-container;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
.group__cover {
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group__tabs {
|
||||||
|
.group__tabs__tab {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 16px 22px;
|
||||||
|
text-align: center;
|
||||||
|
color: $primary-text-color;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&--active {
|
||||||
|
border-bottom: 2px solid $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
clear: both;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
float: right;
|
||||||
|
margin: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
float: right;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group__panel {
|
||||||
|
padding: 10px 10px 20px 10px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group__panel__description {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group__panel__label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: $gab-background-container;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group__feed {
|
||||||
|
background: $gab-background-container;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
53
app/javascript/styles/gabsocial/components/group-form.scss
Normal file
53
app/javascript/styles/gabsocial/components/group-form.scss
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.group-form {
|
||||||
|
background: $gab-background-container;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
.group-form__input {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 15px;
|
||||||
|
display: block;
|
||||||
|
color: $primary-text-color;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid $secondary-text-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form__file-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $secondary-text-color;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 5px;
|
||||||
|
transform: translatey(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.group-form__file-label--selected {
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form__file {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
button {float: right;}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
.group-sidebar-panel {
|
||||||
|
&__items {
|
||||||
|
padding: 0 15px 15px;
|
||||||
|
|
||||||
|
&__show-all {
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: block;
|
||||||
|
color: $primary-text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: $gab-secondary-text;
|
||||||
|
|
||||||
|
&__unread {
|
||||||
|
color: $gab-brand-default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,9 @@
|
|||||||
.user-panel {
|
.user-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 285px;
|
width: 265px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
border-radius: 10px;
|
@include gab-container-standards();
|
||||||
background: $gab-background-container;
|
|
||||||
body.theme-gabsocial-light & {
|
|
||||||
@include light-theme-shadow();
|
|
||||||
background: $gab-background-container-light;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
display: block;
|
display: block;
|
||||||
height: 112px;
|
height: 112px;
|
||||||
|
@ -12,6 +12,7 @@ $gab-brand-default: #21cf7a;
|
|||||||
$gab-alert-red: #cc6643;
|
$gab-alert-red: #cc6643;
|
||||||
$gab-secondary-text: #999;
|
$gab-secondary-text: #999;
|
||||||
$gab-text-highlight: $gab-brand-default;
|
$gab-text-highlight: $gab-brand-default;
|
||||||
|
$gab-small-cta-primary: #607CF5;
|
||||||
|
|
||||||
|
|
||||||
// THEME COLORS
|
// THEME COLORS
|
||||||
@ -28,8 +29,7 @@ $gab-background-container-light: #fff;
|
|||||||
$gab-default-text-light: #6c6c6c;
|
$gab-default-text-light: #6c6c6c;
|
||||||
|
|
||||||
|
|
||||||
// theme mixins
|
|
||||||
@mixin light-theme-shadow() {box-shadow: 0 0 6px 0 rgba(0,0,0,0.1);}
|
|
||||||
|
|
||||||
|
|
||||||
// BREAKPOINT SETS
|
// BREAKPOINT SETS
|
||||||
|
@ -34,6 +34,7 @@ class UserSettingsDecorator
|
|||||||
user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs')
|
user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs')
|
||||||
user.settings['show_application'] = show_application_preference if change?('setting_show_application')
|
user.settings['show_application'] = show_application_preference if change?('setting_show_application')
|
||||||
user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
|
user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
|
||||||
|
user.settings['group_in_home_feed'] = group_in_home_feed_preference if change?('setting_group_in_home_feed')
|
||||||
end
|
end
|
||||||
|
|
||||||
def merged_notification_emails
|
def merged_notification_emails
|
||||||
@ -112,6 +113,10 @@ class UserSettingsDecorator
|
|||||||
boolean_cast_setting 'setting_advanced_layout'
|
boolean_cast_setting 'setting_advanced_layout'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group_in_home_feed_preference
|
||||||
|
boolean_cast_setting 'setting_group_in_home_feed'
|
||||||
|
end
|
||||||
|
|
||||||
def boolean_cast_setting(key)
|
def boolean_cast_setting(key)
|
||||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||||
end
|
end
|
||||||
|
33
app/models/concerns/group_cover_image.rb
Normal file
33
app/models/concerns/group_cover_image.rb
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module GroupCoverImage
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
LIMIT = 4.megabytes
|
||||||
|
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def cover_image_styles(file)
|
||||||
|
styles = { original: { geometry: '1200x475#', file_geometry_parser: FastGeometryParser } }
|
||||||
|
styles[:static] = { geometry: '1200x475#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
|
||||||
|
styles
|
||||||
|
end
|
||||||
|
|
||||||
|
private :cover_image_styles
|
||||||
|
end
|
||||||
|
|
||||||
|
included do
|
||||||
|
has_attached_file :cover_image, styles: ->(f) { cover_image_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail]
|
||||||
|
validates_attachment_content_type :cover_image, content_type: IMAGE_MIME_TYPES
|
||||||
|
validates_attachment_size :cover_image, less_than: LIMIT
|
||||||
|
remotable_attachment :cover_image, LIMIT
|
||||||
|
end
|
||||||
|
|
||||||
|
def cover_image_original_url
|
||||||
|
cover_image.url(:original)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cover_image_static_url
|
||||||
|
cover_image_content_type == 'image/gif' ? cover_image.url(:static) : cover_image_original_url
|
||||||
|
end
|
||||||
|
end
|
@ -13,11 +13,19 @@ module GroupInteractions
|
|||||||
follow_mapping(GroupAccount.where(group_id: target_group_ids, account_id: account_id, role: :admin), :group_id)
|
follow_mapping(GroupAccount.where(group_id: target_group_ids, account_id: account_id, role: :admin), :group_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread_count_map(target_group_ids, account_id)
|
||||||
|
unread_count_mapping(GroupAccount.where(group_id: target_group_ids, account_id: account_id), :unread_count)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def follow_mapping(query, field)
|
def follow_mapping(query, field)
|
||||||
query.pluck(field).each_with_object({}) { |id, mapping| mapping[id] = true }
|
query.pluck(field).each_with_object({}) { |id, mapping| mapping[id] = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread_count_mapping(query, field)
|
||||||
|
query.pluck(:group_id, :unread_count).each_with_object({}) { |e, mapping| mapping[e[0]] = e[1] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -15,11 +15,13 @@
|
|||||||
# is_archived :boolean default(FALSE), not null
|
# is_archived :boolean default(FALSE), not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# member_count :integer default(0)
|
||||||
#
|
#
|
||||||
|
|
||||||
class Group < ApplicationRecord
|
class Group < ApplicationRecord
|
||||||
include Paginable
|
include Paginable
|
||||||
include GroupInteractions
|
include GroupInteractions
|
||||||
|
include GroupCoverImage
|
||||||
|
|
||||||
PER_ACCOUNT_LIMIT = 50
|
PER_ACCOUNT_LIMIT = 50
|
||||||
|
|
||||||
@ -28,17 +30,12 @@ class Group < ApplicationRecord
|
|||||||
has_many :group_accounts, inverse_of: :group, dependent: :destroy
|
has_many :group_accounts, inverse_of: :group, dependent: :destroy
|
||||||
has_many :accounts, through: :group_accounts
|
has_many :accounts, through: :group_accounts
|
||||||
|
|
||||||
|
has_many :group_removed_accounts, inverse_of: :group, dependent: :destroy
|
||||||
|
has_many :removed_accounts, source: :account, through: :group_removed_accounts
|
||||||
|
|
||||||
validates :title, presence: true
|
validates :title, presence: true
|
||||||
validates :description, presence: true
|
validates :description, presence: true
|
||||||
|
|
||||||
LIMIT = 4.megabytes
|
|
||||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
|
||||||
|
|
||||||
has_attached_file :cover_image
|
|
||||||
validates_attachment_content_type :cover_image, content_type: IMAGE_MIME_TYPES
|
|
||||||
validates_attachment_size :cover_image, less_than: LIMIT
|
|
||||||
remotable_attachment :cover_image, LIMIT
|
|
||||||
|
|
||||||
validates_each :account_id, on: :create do |record, _attr, value|
|
validates_each :account_id, on: :create do |record, _attr, value|
|
||||||
record.errors.add(:base, I18n.t('groups.errors.limit')) if Group.where(account_id: value).count >= PER_ACCOUNT_LIMIT
|
record.errors.add(:base, I18n.t('groups.errors.limit')) if Group.where(account_id: value).count >= PER_ACCOUNT_LIMIT
|
||||||
end
|
end
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
# role :string
|
# role :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
# unread_count :integer default(0)
|
||||||
#
|
#
|
||||||
|
|
||||||
class GroupAccount < ApplicationRecord
|
class GroupAccount < ApplicationRecord
|
||||||
@ -18,4 +19,22 @@ class GroupAccount < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
validates :account_id, uniqueness: { scope: :group_id }
|
validates :account_id, uniqueness: { scope: :group_id }
|
||||||
|
|
||||||
|
after_commit :remove_relationship_cache
|
||||||
|
after_create :increment_member_count
|
||||||
|
after_destroy :decrement_member_count
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_relationship_cache
|
||||||
|
Rails.cache.delete("relationship:#{account_id}:group#{group_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def increment_member_count
|
||||||
|
group&.increment!(:member_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
def decrement_member_count
|
||||||
|
group&.decrement!(:member_count)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
15
app/models/group_removed_account.rb
Normal file
15
app/models/group_removed_account.rb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: group_removed_accounts
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# group_id :bigint(8) not null
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class GroupRemovedAccount < ApplicationRecord
|
||||||
|
belongs_to :group
|
||||||
|
belongs_to :account
|
||||||
|
end
|
@ -256,6 +256,7 @@ class Status < ApplicationRecord
|
|||||||
|
|
||||||
after_create_commit :store_uri, if: :local?
|
after_create_commit :store_uri, if: :local?
|
||||||
after_create_commit :update_statistics, if: :local?
|
after_create_commit :update_statistics, if: :local?
|
||||||
|
after_create_commit :increase_group_unread_counts, if: Proc.new { |status| !status.group_id.nil? }
|
||||||
|
|
||||||
around_create GabSocial::Snowflake::Callbacks
|
around_create GabSocial::Snowflake::Callbacks
|
||||||
|
|
||||||
@ -535,4 +536,8 @@ class Status < ApplicationRecord
|
|||||||
AccountConversation.remove_status(inbox_owner, self)
|
AccountConversation.remove_status(inbox_owner, self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def increase_group_unread_counts
|
||||||
|
GroupAccount.where(group_id: group_id).where.not(account_id: account_id).update_all("unread_count = unread_count + 1")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -105,7 +105,7 @@ class User < ApplicationRecord
|
|||||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
||||||
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
||||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||||
:advanced_layout, to: :settings, prefix: :setting, allow_nil: false
|
:advanced_layout, :group_in_home_feed, to: :settings, prefix: :setting, allow_nil: false
|
||||||
|
|
||||||
attr_reader :invite_code
|
attr_reader :invite_code
|
||||||
attr_writer :external
|
attr_writer :external
|
||||||
|
@ -23,7 +23,8 @@ class GroupPolicy < ApplicationPolicy
|
|||||||
|
|
||||||
def join?
|
def join?
|
||||||
check_archive!
|
check_archive!
|
||||||
raise GabSocial::ValidationError, "User is already a member of this group." if is_member?
|
raise GabSocial::ValidationError, "Account is already a member of this group." if is_member?
|
||||||
|
raise GabSocial::ValidationError, "Account is removed from this group." if is_removed?
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@ -42,8 +43,24 @@ class GroupPolicy < ApplicationPolicy
|
|||||||
is_group_admin?
|
is_group_admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show_removed_accounts?
|
||||||
|
is_group_admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_removed_account?
|
||||||
|
is_group_admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_removed_account?
|
||||||
|
is_group_admin?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def is_removed?
|
||||||
|
record.group_removed_accounts.where(account_id: current_account.id).exists?
|
||||||
|
end
|
||||||
|
|
||||||
def is_member?
|
def is_member?
|
||||||
record.group_accounts.where(account_id: current_account.id).exists?
|
record.group_accounts.where(account_id: current_account.id).exists?
|
||||||
end
|
end
|
||||||
|
@ -1,55 +1,15 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class GroupRelationshipsPresenter
|
class GroupRelationshipsPresenter
|
||||||
attr_reader :member, :admin
|
attr_reader :member, :admin, :unread_count
|
||||||
|
|
||||||
def initialize(group_ids, current_account_id, **options)
|
def initialize(group_ids, current_account_id, **options)
|
||||||
@group_ids = group_ids.map { |a| a.is_a?(Group) ? a.id : a }
|
@group_ids = group_ids.map { |a| a.is_a?(Group) ? a.id : a }
|
||||||
@current_account_id = current_account_id
|
@current_account_id = current_account_id
|
||||||
|
|
||||||
@member = cached[:member].merge(Group.member_map(@uncached_group_ids, @current_account_id))
|
@member = Group.member_map(@group_ids, @current_account_id)
|
||||||
@admin = cached[:admin].merge(Group.admin_map(@uncached_group_ids, @current_account_id))
|
@admin = Group.admin_map(@group_ids, @current_account_id)
|
||||||
|
@unread_count = Group.unread_count_map(@group_ids, @current_account_id)
|
||||||
cache_uncached!
|
|
||||||
|
|
||||||
@member.merge!(options[:member_map] || {})
|
|
||||||
@admin.merge!(options[:admin_map] || {})
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def cached
|
|
||||||
return @cached if defined?(@cached)
|
|
||||||
|
|
||||||
@cached = {
|
|
||||||
member: {},
|
|
||||||
admin: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
@uncached_group_ids = []
|
|
||||||
|
|
||||||
@group_ids.each do |group_id|
|
|
||||||
maps_for_group = Rails.cache.read("relationship:#{@current_account_id}:#{group_id}")
|
|
||||||
|
|
||||||
if maps_for_group.is_a?(Hash)
|
|
||||||
@cached.deep_merge!(maps_for_group)
|
|
||||||
else
|
|
||||||
@uncached_group_ids << group_id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@cached
|
|
||||||
end
|
|
||||||
|
|
||||||
def cache_uncached!
|
|
||||||
@uncached_group_ids.each do |group_id|
|
|
||||||
maps_for_account = {
|
|
||||||
member: { group_id => member[group_id] },
|
|
||||||
admin: { group_id => admin[group_id] },
|
|
||||||
}
|
|
||||||
|
|
||||||
Rails.cache.write("relationship:#{@current_account_id}:#{group_id}", maps_for_account, expires_in: 1.day)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -33,6 +33,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||||||
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
|
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
|
||||||
store[:reduce_motion] = object.current_account.user.setting_reduce_motion
|
store[:reduce_motion] = object.current_account.user.setting_reduce_motion
|
||||||
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
||||||
|
store[:group_in_home_feed] = object.current_account.user.setting_group_in_home_feed
|
||||||
store[:is_staff] = object.current_account.user.staff?
|
store[:is_staff] = object.current_account.user.staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::GroupRelationshipSerializer < ActiveModel::Serializer
|
class REST::GroupRelationshipSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :member, :admin
|
attributes :id, :member, :admin, :unread_count
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
@ -14,4 +14,8 @@ class REST::GroupRelationshipSerializer < ActiveModel::Serializer
|
|||||||
def admin
|
def admin
|
||||||
instance_options[:relationships].admin[object.id] ? true : false
|
instance_options[:relationships].admin[object.id] ? true : false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread_count
|
||||||
|
instance_options[:relationships].unread_count[object.id] || 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,13 +3,23 @@
|
|||||||
class REST::GroupSerializer < ActiveModel::Serializer
|
class REST::GroupSerializer < ActiveModel::Serializer
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :id, :title, :description, :cover_image_url, :is_archived
|
attributes :id, :title, :description, :cover_image_url, :is_archived, :member_count
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clean_migrated_url
|
||||||
|
object
|
||||||
|
.cover_image_file_name
|
||||||
|
.sub("gab://groups/", "https://gab.com/media/user/")
|
||||||
|
end
|
||||||
|
|
||||||
def cover_image_url
|
def cover_image_url
|
||||||
|
if object.cover_image_file_name and object.cover_image_file_name.start_with? "gab://groups/"
|
||||||
|
return clean_migrated_url
|
||||||
|
end
|
||||||
|
|
||||||
full_asset_url(object.cover_image.url)
|
full_asset_url(object.cover_image.url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||||||
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
|
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
|
||||||
:sensitive, :spoiler_text, :visibility, :language,
|
:sensitive, :spoiler_text, :visibility, :language,
|
||||||
:uri, :url, :replies_count, :reblogs_count,
|
:uri, :url, :replies_count, :reblogs_count,
|
||||||
:favourites_count
|
:favourites_count, :group_id
|
||||||
|
|
||||||
attribute :favourited, if: :current_user?
|
attribute :favourited, if: :current_user?
|
||||||
attribute :reblogged, if: :current_user?
|
attribute :reblogged, if: :current_user?
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
class GroupQueryService < BaseService
|
class GroupQueryService < BaseService
|
||||||
def call(group)
|
def call(group)
|
||||||
Status.distinct
|
Status.as_group_timeline(group)
|
||||||
.as_group_timeline(group)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
= f.input :setting_expand_spoilers, as: :boolean, wrapper: :with_label
|
= f.input :setting_expand_spoilers, as: :boolean, wrapper: :with_label
|
||||||
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
||||||
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
||||||
|
= f.input :setting_group_in_home_feed, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
@ -105,6 +105,7 @@ en:
|
|||||||
setting_hide_network: Hide your network
|
setting_hide_network: Hide your network
|
||||||
setting_noindex: Opt-out of search engine indexing
|
setting_noindex: Opt-out of search engine indexing
|
||||||
setting_reduce_motion: Reduce motion in animations
|
setting_reduce_motion: Reduce motion in animations
|
||||||
|
setting_group_in_home_feed: Show posts from your groups
|
||||||
setting_show_application: Disclose application used to send gabs
|
setting_show_application: Disclose application used to send gabs
|
||||||
setting_system_font_ui: Use system's default font
|
setting_system_font_ui: Use system's default font
|
||||||
setting_theme: Site theme
|
setting_theme: Site theme
|
||||||
|
@ -404,6 +404,7 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :relationships, only: :index, controller: 'groups/relationships'
|
resources :relationships, only: :index, controller: 'groups/relationships'
|
||||||
resource :accounts, only: [:show, :create, :update, :destroy], controller: 'groups/accounts'
|
resource :accounts, only: [:show, :create, :update, :destroy], controller: 'groups/accounts'
|
||||||
|
resource :removed_accounts, only: [:show, :create, :destroy], controller: 'groups/removed_accounts'
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :polls, only: [:create, :show] do
|
resources :polls, only: [:create, :show] do
|
||||||
|
12
db/migrate/20190716173227_create_group_removed_accounts.rb
Normal file
12
db/migrate/20190716173227_create_group_removed_accounts.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class CreateGroupRemovedAccounts < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :group_removed_accounts do |t|
|
||||||
|
t.belongs_to :group, foreign_key: { on_delete: :cascade }, null: false
|
||||||
|
t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :group_removed_accounts, [:account_id, :group_id], unique: true
|
||||||
|
add_index :group_removed_accounts, [:group_id, :account_id]
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,10 @@
|
|||||||
|
class AddUnreadCountToGroupAccounts < ActiveRecord::Migration[5.2]
|
||||||
|
def up
|
||||||
|
add_column :group_accounts, :unread_count, :integer
|
||||||
|
change_column_default :group_accounts, :unread_count, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :group_accounts, :unread_count
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,10 @@
|
|||||||
|
class BackfillAddUnreadCountToGroupAccounts < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
GroupAccount.in_batches do |relation|
|
||||||
|
relation.update_all unread_count: 0
|
||||||
|
sleep(0.1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
db/migrate/20190722003541_add_member_count_to_groups.rb
Normal file
10
db/migrate/20190722003541_add_member_count_to_groups.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class AddMemberCountToGroups < ActiveRecord::Migration[5.2]
|
||||||
|
def up
|
||||||
|
add_column :groups, :member_count, :integer
|
||||||
|
change_column_default :groups, :member_count, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :groups, :member_count
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,10 @@
|
|||||||
|
class BackfillAddMemberCountToGroups < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
Group.in_batches do |relation|
|
||||||
|
relation.update_all member_count: 0
|
||||||
|
sleep(0.1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
db/schema.rb
27
db/schema.rb
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2019_06_07_000211) do
|
ActiveRecord::Schema.define(version: 2019_07_22_003649) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -93,7 +93,7 @@ ActiveRecord::Schema.define(version: 2019_06_07_000211) do
|
|||||||
t.bigint "account_id"
|
t.bigint "account_id"
|
||||||
t.string "image_file_name"
|
t.string "image_file_name"
|
||||||
t.string "image_content_type"
|
t.string "image_content_type"
|
||||||
t.bigint "image_file_size"
|
t.integer "image_file_size"
|
||||||
t.datetime "image_updated_at"
|
t.datetime "image_updated_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
@ -157,10 +157,10 @@ ActiveRecord::Schema.define(version: 2019_06_07_000211) do
|
|||||||
t.string "actor_type"
|
t.string "actor_type"
|
||||||
t.boolean "discoverable"
|
t.boolean "discoverable"
|
||||||
t.string "also_known_as", array: true
|
t.string "also_known_as", array: true
|
||||||
t.datetime "silenced_at"
|
|
||||||
t.datetime "suspended_at"
|
|
||||||
t.boolean "is_pro", default: false, null: false
|
t.boolean "is_pro", default: false, null: false
|
||||||
t.datetime "pro_expires_at"
|
t.datetime "pro_expires_at"
|
||||||
|
t.datetime "silenced_at"
|
||||||
|
t.datetime "suspended_at"
|
||||||
t.boolean "is_verified", default: false, null: false
|
t.boolean "is_verified", default: false, null: false
|
||||||
t.boolean "is_donor", default: false, null: false
|
t.boolean "is_donor", default: false, null: false
|
||||||
t.boolean "is_investor", default: false, null: false
|
t.boolean "is_investor", default: false, null: false
|
||||||
@ -327,12 +327,24 @@ ActiveRecord::Schema.define(version: 2019_06_07_000211) do
|
|||||||
t.string "role"
|
t.string "role"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.integer "unread_count", default: 0
|
||||||
t.index ["account_id", "group_id"], name: "index_group_accounts_on_account_id_and_group_id", unique: true
|
t.index ["account_id", "group_id"], name: "index_group_accounts_on_account_id_and_group_id", unique: true
|
||||||
t.index ["account_id"], name: "index_group_accounts_on_account_id"
|
t.index ["account_id"], name: "index_group_accounts_on_account_id"
|
||||||
t.index ["group_id", "account_id"], name: "index_group_accounts_on_group_id_and_account_id"
|
t.index ["group_id", "account_id"], name: "index_group_accounts_on_group_id_and_account_id"
|
||||||
t.index ["group_id"], name: "index_group_accounts_on_group_id"
|
t.index ["group_id"], name: "index_group_accounts_on_group_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "group_removed_accounts", force: :cascade do |t|
|
||||||
|
t.bigint "group_id", null: false
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["account_id", "group_id"], name: "index_group_removed_accounts_on_account_id_and_group_id", unique: true
|
||||||
|
t.index ["account_id"], name: "index_group_removed_accounts_on_account_id"
|
||||||
|
t.index ["group_id", "account_id"], name: "index_group_removed_accounts_on_group_id_and_account_id"
|
||||||
|
t.index ["group_id"], name: "index_group_removed_accounts_on_group_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "groups", force: :cascade do |t|
|
create_table "groups", force: :cascade do |t|
|
||||||
t.bigint "account_id"
|
t.bigint "account_id"
|
||||||
t.string "title", null: false
|
t.string "title", null: false
|
||||||
@ -346,6 +358,7 @@ ActiveRecord::Schema.define(version: 2019_06_07_000211) do
|
|||||||
t.boolean "is_archived", default: false, null: false
|
t.boolean "is_archived", default: false, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.integer "member_count", default: 0
|
||||||
t.index ["account_id"], name: "index_groups_on_account_id"
|
t.index ["account_id"], name: "index_groups_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -645,8 +658,8 @@ ActiveRecord::Schema.define(version: 2019_06_07_000211) do
|
|||||||
create_table "status_pins", force: :cascade do |t|
|
create_table "status_pins", force: :cascade do |t|
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.bigint "status_id", null: false
|
t.bigint "status_id", null: false
|
||||||
t.datetime "created_at", default: -> { "now()" }, null: false
|
t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||||
t.datetime "updated_at", default: -> { "now()" }, null: false
|
t.datetime "updated_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||||
t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true
|
t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -849,6 +862,8 @@ ActiveRecord::Schema.define(version: 2019_06_07_000211) do
|
|||||||
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
||||||
add_foreign_key "group_accounts", "accounts", on_delete: :cascade
|
add_foreign_key "group_accounts", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "group_accounts", "groups", on_delete: :cascade
|
add_foreign_key "group_accounts", "groups", on_delete: :cascade
|
||||||
|
add_foreign_key "group_removed_accounts", "accounts", on_delete: :cascade
|
||||||
|
add_foreign_key "group_removed_accounts", "groups", on_delete: :cascade
|
||||||
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
||||||
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
|
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
|
||||||
add_foreign_key "invites", "users", on_delete: :cascade
|
add_foreign_key "invites", "users", on_delete: :cascade
|
||||||
|
12
lib/tasks/fix_group_member_counts.rake
Normal file
12
lib/tasks/fix_group_member_counts.rake
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
task fix_group_member_counts: 'gabsocial:fix-group-member-counts'
|
||||||
|
|
||||||
|
namespace :gabsocial do
|
||||||
|
desc 'Re-compute group member counts'
|
||||||
|
task :fix_group_member_counts => :environment do
|
||||||
|
Group.select(:id).all.each do |group|
|
||||||
|
group.update_column(:member_count, group.accounts.count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
2
spec/fabricators/group_removed_account_fabricator.rb
Normal file
2
spec/fabricators/group_removed_account_fabricator.rb
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Fabricator(:group_removed_account) do
|
||||||
|
end
|
5
spec/models/group_removed_account_spec.rb
Normal file
5
spec/models/group_removed_account_spec.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe GroupRemovedAccount, type: :model do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user