diff --git a/app/controllers/api/v1/groups/pins_controller.rb b/app/controllers/api/v1/groups/pins_controller.rb new file mode 100644 index 00000000..850ba653 --- /dev/null +++ b/app/controllers/api/v1/groups/pins_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class Api::V1::Groups::PinsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:groups' } + before_action :require_user! + before_action :set_group + before_action :set_status + + respond_to :json + + def create + GroupPinnedStatus.create!(group: @group, status: @status) + render json: @status, serializer: REST::StatusSerializer + end + + def destroy + pin = GroupPinnedStatus.find_by(group: @group, status: @status) + + if pin + pin.destroy! + end + + render json: @status, serializer: REST::StatusSerializer + end + + private + + def set_status + @status = Status.find(params[:statusId]) + end + + def set_group + @group = Group.find(params[:group_id]) + end + +end diff --git a/app/controllers/api/v1/timelines/group_controller.rb b/app/controllers/api/v1/timelines/group_controller.rb index dd726d9f..e9c870a2 100644 --- a/app/controllers/api/v1/timelines/group_controller.rb +++ b/app/controllers/api/v1/timelines/group_controller.rb @@ -13,7 +13,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController if current_user render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id) + relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id, group_id: @group.id) else render json: @statuses, each_serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1/timelines/group_pins_controller.rb b/app/controllers/api/v1/timelines/group_pins_controller.rb new file mode 100644 index 00000000..bf8353d9 --- /dev/null +++ b/app/controllers/api/v1/timelines/group_pins_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class Api::V1::Timelines::GroupPinsController < Api::BaseController + before_action :set_group + before_action :set_statuses + + def show + if current_user + render json: @statuses, + each_serializer: REST::StatusSerializer, + relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id, group_id: @group.id) + else + render json: @statuses, each_serializer: REST::StatusSerializer + end + end + + private + + def set_group + @group = Group.where(id: params[:id], is_archived: false).first + end + + def set_statuses + @statuses = cached_group_statuses + end + + def cached_group_statuses + cache_collection group_statuses, Status + end + + def group_statuses + statuses = [] + @group.pinned_statuses.each do |s| + statuses << s + end + statuses + end + +end diff --git a/app/javascript/gabsocial/actions/groups.js b/app/javascript/gabsocial/actions/groups.js index 2a99f4d4..f273cd4b 100644 --- a/app/javascript/gabsocial/actions/groups.js +++ b/app/javascript/gabsocial/actions/groups.js @@ -63,6 +63,14 @@ export const GROUP_UPDATE_ROLE_REQUEST = 'GROUP_UPDATE_ROLE_REQUEST'; export const GROUP_UPDATE_ROLE_SUCCESS = 'GROUP_UPDATE_ROLE_SUCCESS'; export const GROUP_UPDATE_ROLE_FAIL = 'GROUP_UPDATE_ROLE_FAIL'; +export const GROUP_PIN_STATUS_REQUEST = 'GROUP_PIN_STATUS_REQUEST' +export const GROUP_PIN_STATUS_SUCCESS = 'GROUP_PIN_STATUS_SUCCESS' +export const GROUP_PIN_STATUS_FAIL = 'GROUP_PIN_STATUS_FAIL' + +export const GROUP_UNPIN_STATUS_REQUEST = 'GROUP_UNPIN_STATUS_REQUEST' +export const GROUP_UNPIN_STATUS_SUCCESS = 'GROUP_UNPIN_STATUS_SUCCESS' +export const GROUP_UNPIN_STATUS_FAIL = 'GROUP_UNPIN_STATUS_FAIL =' + export const GROUP_TIMELINE_SORT = 'GROUP_TIMELINE_SORT' export const GROUP_TIMELINE_TOP_SORT = 'GROUP_TIMELINE_TOP_SORT' @@ -587,6 +595,83 @@ export function updateRoleFail(groupId, id, error) { }; }; + +export function pinGroupStatus(groupId, statusId) { + return (dispatch, getState) => { + if (!me) return + + dispatch(pinGroupStatusRequest(groupId)) + + api(getState).post(`/api/v1/groups/${groupId}/pin`, { statusId }).then((response) => { + dispatch(pinGroupStatusSuccess(groupId, statusId)) + }).catch((error) => { + dispatch(pinGroupStatusFail(groupId, statusId, error)) + }) + } +} + +export function pinGroupStatusRequest(groupId) { + return { + type: GROUP_PIN_STATUS_REQUEST, + groupId, + } +} + +export function pinGroupStatusSuccess(groupId, statusId) { + return { + type: GROUP_PIN_STATUS_SUCCESS, + groupId, + statusId, + } +} + +export function pinGroupStatusFail(groupId, statusId, error) { + return { + type: GROUP_PIN_STATUS_FAIL, + groupId, + statusId, + error, + } +} + +export function unpinGroupStatus(groupId, statusId) { + return (dispatch, getState) => { + if (!me) return + + dispatch(unpinGroupStatusRequest(groupId)) + + api(getState).post(`/api/v1/groups/${groupId}/unpin`, { statusId }).then((response) => { + dispatch(unpinGroupStatusSuccess(groupId, statusId)) + }).catch((error) => { + dispatch(unpinGroupStatusFail(groupId, statusId, error)) + }) + } +} + +export function unpinGroupStatusRequest(groupId) { + return { + type: GROUP_UNPIN_STATUS_REQUEST, + groupId, + } +} + +export function unpinGroupStatusSuccess(groupId, statusId) { + return { + type: GROUP_UNPIN_STATUS_SUCCESS, + groupId, + statusId, + } +} + +export function unpinGroupStatusFail(groupId, statusId, error) { + return { + type: GROUP_UNPIN_STATUS_FAIL, + groupId, + statusId, + error, + } +} + export const sortGroups = (tab, sortType) => (dispatch, getState) => { const groupIdsByTab = getState().getIn(['group_lists', tab, 'items'], ImmutableList()).toJS() const allGroups = getState().get('groups', ImmutableMap()).toJS() diff --git a/app/javascript/gabsocial/actions/timelines.js b/app/javascript/gabsocial/actions/timelines.js index 798e0e54..7e61550c 100644 --- a/app/javascript/gabsocial/actions/timelines.js +++ b/app/javascript/gabsocial/actions/timelines.js @@ -173,6 +173,7 @@ export const expandAccountFeaturedTimeline = accountId => expandTimeline(`accoun export const expandAccountMediaTimeline = (accountId, { maxId, limit, mediaType } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: limit || 20, media_type: mediaType }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandGroupTimeline = (id, { sortBy, maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { sort_by: sortBy, max_id: maxId, only_media: onlyMedia }, done); +export const expandGroupFeaturedTimeline = (groupId, done = noOp) => expandTimeline(`group:${groupId}:pinned`, `/api/v1/timelines/group_pins/${groupId}`, {}, done); export const expandGroupCollectionTimeline = (collectionType, { sortBy, maxId } = {}, done = noOp) => expandTimeline(`group_collection:${collectionType}`, `/api/v1/timelines/group_collection/${collectionType}`, { sort_by: sortBy, max_id: maxId }, done); export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { diff --git a/app/javascript/gabsocial/components/popover/status_options_popover.js b/app/javascript/gabsocial/components/popover/status_options_popover.js index 33d22f8c..aeb7a15e 100644 --- a/app/javascript/gabsocial/components/popover/status_options_popover.js +++ b/app/javascript/gabsocial/components/popover/status_options_popover.js @@ -24,6 +24,8 @@ import { fetchGroupRelationships, createRemovedAccount, groupRemoveStatus, + pinGroupStatus, + unpinGroupStatus, } from '../../actions/groups' import { initMuteModal } from '../../actions/mutes' import { initReport } from '../../actions/reports' @@ -86,6 +88,10 @@ class StatusOptionsPopover extends ImmutablePureComponent { this.props.onPin(this.props.status) } + handleGroupPinStatus = () => { + this.props.onPinGroupStatus(this.props.status) + } + handleBookmarkClick = () => { if (this.props.isPro) { this.props.onBookmark(this.props.status) @@ -230,6 +236,29 @@ class StatusOptionsPopover extends ImmutablePureComponent { } } + if (withGroupAdmin) { + menu.push(null) + menu.push({ + icon: 'trash', + hideArrow: true, + title: intl.formatMessage(messages.group_remove_account), + onClick: this.handleGroupRemoveAccount, + }) + menu.push({ + icon: 'trash', + hideArrow: true, + title: intl.formatMessage(messages.group_remove_post), + onClick: this.handleGroupRemovePost, + }) + menu.push(null) + menu.push({ + icon: 'pin', + hideArrow: true, + title: intl.formatMessage(status.get('pinned_by_group') ? messages.groupUnpin : messages.groupPin), + onClick: this.handleGroupPinStatus, + }) + } + menu.push(null) menu.push({ icon: 'copy', @@ -249,22 +278,6 @@ class StatusOptionsPopover extends ImmutablePureComponent { title: intl.formatMessage(messages.embed), onClick: this.handleOnOpenEmbedModal, }) - - if (withGroupAdmin) { - menu.push(null) - menu.push({ - icon: 'trash', - hideArrow: true, - title: intl.formatMessage(messages.group_remove_account), - onClick: this.handleGroupRemoveAccount, - }) - menu.push({ - icon: 'trash', - hideArrow: true, - title: intl.formatMessage(messages.group_remove_post), - onClick: this.handleGroupRemovePost, - }) - } if (isStaff) { menu.push(null) @@ -316,6 +329,8 @@ const messages = defineMessages({ unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, + groupPin: { id: 'status.group_pin', defaultMessage: 'Pin in group' }, + groupUnpin: { id: 'status.group_unpin', defaultMessage: 'Unpin from group' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark status' }, unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, @@ -471,6 +486,16 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(openModal(MODAL_PRO_UPGRADE)) }, + onPinGroupStatus(status) { + dispatch(closePopover()) + + if (status.get('pinned_by_group')) { + dispatch(unpinGroupStatus(status.getIn(['group', 'id']), status.get('id'))) + } else { + dispatch(pinGroupStatus(status.getIn(['group', 'id']), status.get('id'))) + } + }, + onClosePopover: () => dispatch(closePopover()), }) diff --git a/app/javascript/gabsocial/components/status.js b/app/javascript/gabsocial/components/status.js index 8e97c3c7..731a618a 100644 --- a/app/javascript/gabsocial/components/status.js +++ b/app/javascript/gabsocial/components/status.js @@ -82,6 +82,7 @@ class Status extends ImmutablePureComponent { 'isChild', 'isPromoted', 'isFeatured', + 'isPinnedInGroup', 'isMuted', 'isHidden', 'isIntersecting', @@ -311,6 +312,7 @@ class Status extends ImmutablePureComponent { const { intl, isFeatured, + isPinnedInGroup, isPromoted, isChild, isHidden, @@ -429,7 +431,7 @@ class Status extends ImmutablePureComponent {
@@ -564,6 +567,7 @@ Status.propTypes = { isChild: PropTypes.bool, isPromoted: PropTypes.bool, isFeatured: PropTypes.bool, + isPinnedInGroup: PropTypes.bool, isMuted: PropTypes.bool, isHidden: PropTypes.bool, isIntersecting: PropTypes.bool, diff --git a/app/javascript/gabsocial/components/status_list.js b/app/javascript/gabsocial/components/status_list.js index 9261ee69..de32e369 100644 --- a/app/javascript/gabsocial/components/status_list.js +++ b/app/javascript/gabsocial/components/status_list.js @@ -67,11 +67,18 @@ class StatusList extends ImmutablePureComponent { } getFeaturedStatusCount = () => { + if (!!this.props.groupPinnedStatusIds) { + return this.props.groupPinnedStatusIds.size + } + return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0 } getCurrentStatusIndex = (id, featured) => { if (featured) { + if (!!this.props.groupPinnedStatusIds) { + return this.props.groupPinnedStatusIds.indexOf(id) + } return this.props.featuredStatusIds.indexOf(id) } @@ -129,6 +136,7 @@ class StatusList extends ImmutablePureComponent { const { statusIds, featuredStatusIds, + groupPinnedStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, @@ -225,6 +233,20 @@ class StatusList extends ImmutablePureComponent { )).concat(scrollableContent) } + if (scrollableContent && groupPinnedStatusIds) { + scrollableContent = groupPinnedStatusIds.map((statusId) => ( + + )).concat(scrollableContent) + } + return ( - {intl.formatMessage(isFeatured ? messages.pinned : messages.promoted)} + {intl.formatMessage(isFeatured ? messages.pinned : isPinnedInGroup ? messages.pinnedByGroup : messages.promoted)} } { @@ -99,6 +100,7 @@ const messages = defineMessages({ filtered: { id: 'status.filtered', defaultMessage: 'Filtered' }, promoted: { id: 'status.promoted', defaultMessage: 'Promoted gab' }, pinned: { id: 'status.pinned', defaultMessage: 'Pinned gab' }, + pinnedByGroup: { id: 'status.pinned_by_group', defaultMessage: 'Pinned to group' }, reposted: { id: 'status.reposted_by', defaultMessage: '{name} reposted' }, }) @@ -107,6 +109,7 @@ StatusPrepend.propTypes = { status: ImmutablePropTypes.map, isComment: PropTypes.bool, isFeatured: PropTypes.bool, + isPinnedInGroup: PropTypes.bool, isPromoted: PropTypes.bool, } diff --git a/app/javascript/gabsocial/features/group_timeline.js b/app/javascript/gabsocial/features/group_timeline.js index 53106e03..40576d2c 100644 --- a/app/javascript/gabsocial/features/group_timeline.js +++ b/app/javascript/gabsocial/features/group_timeline.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePropTypes from 'react-immutable-proptypes' +import { List as ImmutableList } from 'immutable' import { injectIntl, defineMessages } from 'react-intl' import { me } from '../initial_state' import getSortBy from '../utils/group_sort_by' @@ -10,6 +11,7 @@ import { connectGroupStream } from '../actions/streaming' import { clearTimeline, expandGroupTimeline, + expandGroupFeaturedTimeline, } from '../actions/timelines' import { setGroupTimelineSort, @@ -41,6 +43,8 @@ class GroupTimeline extends ImmutablePureComponent { this.props.setMemberNewest() } else { const sortBy = getSortBy(sortByValue, sortByTopValue, onlyMedia) + + this.props.onExpandGroupFeaturedTimeline(groupId) this.props.onExpandGroupTimeline(groupId, { sortBy, onlyMedia }) if (!!me) { @@ -57,6 +61,10 @@ class GroupTimeline extends ImmutablePureComponent { this.handleLoadMore() this.props.onClearTimeline(`group:${this.props.groupId}`) } + + if (prevProps.groupId !== this.props.groupId) { + this.props.onExpandGroupFeaturedTimeline(this.props.groupId) + } } componentWillUnmount() { @@ -83,6 +91,7 @@ class GroupTimeline extends ImmutablePureComponent { group, groupId, intl, + groupPinnedStatusIds, } = this.props if (typeof group === 'undefined') { @@ -98,6 +107,7 @@ class GroupTimeline extends ImmutablePureComponent { scrollKey={`group-timeline-${groupId}`} timelineId={`group:${groupId}`} onLoadMore={this.handleLoadMore} + groupPinnedStatusIds={groupPinnedStatusIds} emptyMessage={intl.formatMessage(messages.empty)} /> @@ -110,9 +120,12 @@ const messages = defineMessages({ empty: { id: 'empty_column.group', defaultMessage: 'There is nothing in this group yet.\nWhen members of this group post new statuses, they will appear here.' }, }) +const emptyList = ImmutableList() + const mapStateToProps = (state, props) => ({ groupId: props.params.id, group: state.getIn(['groups', props.params.id]), + groupPinnedStatusIds: state.getIn(['timelines', `group:${props.params.id}:pinned`, 'items'], emptyList), sortByValue: state.getIn(['group_lists', 'sortByValue']), sortByTopValue: state.getIn(['group_lists', 'sortByTopValue']), }) @@ -130,6 +143,9 @@ const mapDispatchToProps = (dispatch) => ({ setMemberNewest() { dispatch(setGroupTimelineSort(GROUP_TIMELINE_SORTING_TYPE_NEWEST)) }, + onExpandGroupFeaturedTimeline(groupId) { + dispatch(expandGroupFeaturedTimeline(groupId)) + }, }) GroupTimeline.propTypes = { @@ -143,6 +159,7 @@ GroupTimeline.propTypes = { onConnectGroupStream: PropTypes.func.isRequired, onClearTimeline: PropTypes.func.isRequired, onExpandGroupTimeline: PropTypes.func.isRequired, + onExpandGroupFeaturedTimeline: PropTypes.func.isRequired, setMemberNewest: PropTypes.func.isRequired, sortByValue: PropTypes.string.isRequired, sortByTopValue: PropTypes.string, diff --git a/app/models/concerns/group_interactions.rb b/app/models/concerns/group_interactions.rb index 6b856599..6bb973bd 100644 --- a/app/models/concerns/group_interactions.rb +++ b/app/models/concerns/group_interactions.rb @@ -17,6 +17,10 @@ module GroupInteractions follow_mapping(GroupAccount.where(group_id: target_group_ids, account_id: account_id, role: :moderator), :group_id) end + def pinned?(status) + group_pinned_statuses.where(group_id: status.group_id, status: status).exists? + end + private def follow_mapping(query, field) diff --git a/app/models/group.rb b/app/models/group.rb index ea23bdd7..5f2cced8 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -36,6 +36,9 @@ class Group < ApplicationRecord has_many :group_accounts, inverse_of: :group, dependent: :destroy has_many :accounts, through: :group_accounts + has_many :group_pinned_statuses, inverse_of: :group, dependent: :destroy + has_many :pinned_statuses, source: :status, through: :group_pinned_statuses + has_many :group_removed_accounts, inverse_of: :group, dependent: :destroy has_many :removed_accounts, source: :account, through: :group_removed_accounts diff --git a/app/models/group_pinned_status.rb b/app/models/group_pinned_status.rb new file mode 100644 index 00000000..e48a2765 --- /dev/null +++ b/app/models/group_pinned_status.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: group_pinned_statuses +# +# id :bigint(8) not null, primary key +# status_id :bigint(8) not null +# group_id :bigint(8) not null +# + +class GroupPinnedStatus < ApplicationRecord + belongs_to :group + belongs_to :status + + validates_with GroupPinnedStatusValidator +end diff --git a/app/models/status.rb b/app/models/status.rb index ebfb902a..c4588e11 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -389,6 +389,12 @@ class Status < ApplicationRecord StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true } end + def group_pins_map(status_ids, group_id = nil) + unless group_id.nil? + GroupPinnedStatus.select('status_id').where(status_id: status_ids).where(group_id: group_id).each_with_object({}) { |p, h| h[p.status_id] = true } + end + end + def reload_stale_associations!(cached_items) account_ids = [] diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index ce46d7d9..6847e2b5 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true class StatusRelationshipsPresenter - attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map, :bookmarks_map - + attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map, :group_pins_map, :bookmarks_map def initialize(statuses, current_account_id = nil, **options) if current_account_id.nil? @@ -11,6 +10,7 @@ class StatusRelationshipsPresenter @mutes_map = {} @bookmarks_map = {} @pins_map = {} + @group_pins_map = {} else statuses = statuses.compact status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id, s.quote_of_id] }.uniq.compact @@ -22,6 +22,11 @@ class StatusRelationshipsPresenter @mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(options[:mutes_map] || {}) @bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {}) @pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {}) + if options[:group_id] + @group_pins_map = Status.group_pins_map(status_ids, options[:group_id]).merge(options[:group_pins_map] || {}) + else + @group_pins_map = {} + end end end end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 7d91d0dd..b8aed7e4 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -11,6 +11,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :muted, if: :current_user? attribute :bookmarked, if: :current_user? attribute :pinned, if: :pinnable? + attribute :pinned_by_group, if: :pinnable_by_group? attribute :content, unless: :source_requested? attribute :rich_content, unless: :source_requested? @@ -141,6 +142,22 @@ class REST::StatusSerializer < ActiveModel::Serializer %w(public unlisted).include?(object.visibility) end + def pinned_by_group + if instance_options && instance_options[:relationships] + instance_options[:relationships].group_pins_map[object.id] || false + else + false + end + end + + def pinnable_by_group? + if object.group_id? + true + else + false + end + end + def source_requested? instance_options[:source_requested] end diff --git a/app/validators/group_pinned_status_validator.rb b/app/validators/group_pinned_status_validator.rb new file mode 100644 index 00000000..37483cfa --- /dev/null +++ b/app/validators/group_pinned_status_validator.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class GroupPinnedStatusValidator < ActiveModel::Validator + def validate(groupPin) + groupPin.errors.add(:base, I18n.t('statuses.group_pin_errors.reblog')) if groupPin.status.reblog? + groupPin.errors.add(:base, I18n.t('statuses.pin_errors.ungrouped')) unless groupPin.status.group_id + groupPin.errors.add(:base, I18n.t('statuses.pin_errors.notGroupStatus')) if groupPin.status.group_id != groupPin.group.id + groupPin.errors.add(:base, I18n.t('statuses.group_pin_errors.limit')) if groupPin.group.group_pinned_statuses.count >= 4 + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index da382a3b..ddab0e43 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -933,6 +933,11 @@ en: ownership: Someone else's gab cannot be pinned private: Non-public gabs can't be pinned reblog: A repost cannot be pinned + group_pin_errors: + limit: You have already pinned the maximum number of gabs for groups + ungrouped: Gabs not made in the group cannot be pinned + reblog: A repost cannot be pinned + notGroupStatus: The status you are attempting to pin is not associated with the given group poll: total_votes: one: "%{count} vote" diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml index 5816e6c5..5ec7f83c 100644 --- a/config/locales/en_GB.yml +++ b/config/locales/en_GB.yml @@ -873,6 +873,11 @@ en_GB: ownership: Someone else's toot cannot be pinned private: Non-public toot cannot be pinned reblog: A repost cannot be pinned + group_pin_errors: + limit: You have already pinned the maximum number of gabs for groups + ungrouped: Gabs not made in the group cannot be pinned + reblog: A repost cannot be pinned + notGroupStatus: The status you are attempting to pin is not associated with the given group poll: total_votes: one: "%{count} vote" diff --git a/config/routes.rb b/config/routes.rb index 792c1558..829feb4f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -330,6 +330,7 @@ Rails.application.routes.draw do resources :list, only: :show resources :group, only: :show resources :group_collection, only: :show + resources :group_pins, only: :show end resources :gab_trends, only: [:index] @@ -439,6 +440,8 @@ Rails.application.routes.draw do resources :relationships, only: :index, controller: 'groups/relationships' resource :accounts, only: [:show, :create, :update, :destroy], controller: 'groups/accounts' resource :removed_accounts, only: [:show, :create, :destroy], controller: 'groups/removed_accounts' + resource :pin, only: :create, controller: 'groups/pins' + post :unpin, to: 'groups/pins#destroy' end resources :polls, only: [:create, :show] do