From 044e9695fedbf7ebef0c57bfebd552d3aff3f8f7 Mon Sep 17 00:00:00 2001
From: 2458773093 <2458773093@protonmail.com>
Date: Sat, 31 Aug 2019 00:14:50 +0300
Subject: [PATCH 1/5] model/migration for promoted statuses
---
app/models/promotion.rb | 18 ++++++++++++++++
.../20190903162122_create_promotions.rb | 11 ++++++++++
db/schema.rb | 21 +++++++++++++------
3 files changed, 44 insertions(+), 6 deletions(-)
create mode 100644 app/models/promotion.rb
create mode 100644 db/migrate/20190903162122_create_promotions.rb
diff --git a/app/models/promotion.rb b/app/models/promotion.rb
new file mode 100644
index 00000000..6ae948b6
--- /dev/null
+++ b/app/models/promotion.rb
@@ -0,0 +1,18 @@
+# == Schema Information
+#
+# Table name: promotions
+#
+# id :bigint(8) not null, primary key
+# created_at :datetime not null
+# updated_at :datetime not null
+# expires_at :datetime
+# status_id :bigint(8) not null
+# timeline_id :string
+# position :integer default(10)
+#
+
+class Promotion < ApplicationRecord
+ belongs_to :status
+
+ scope :active, -> { where('expires_at > ?', [Time.now]) }
+end
diff --git a/db/migrate/20190903162122_create_promotions.rb b/db/migrate/20190903162122_create_promotions.rb
new file mode 100644
index 00000000..1adea431
--- /dev/null
+++ b/db/migrate/20190903162122_create_promotions.rb
@@ -0,0 +1,11 @@
+class CreatePromotions < ActiveRecord::Migration[5.2]
+ def change
+ create_table :promotions do |t|
+ t.timestamps
+ t.datetime :expires_at, null: true
+ t.bigint :status_id, null: false
+ t.string :timeline_id, null: true
+ t.integer :position, default: 10
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 04b3db48..ac9420ea 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_08_04_115634) do
+ActiveRecord::Schema.define(version: 2019_09_03_162122) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -93,7 +93,7 @@ ActiveRecord::Schema.define(version: 2019_08_04_115634) do
t.bigint "account_id"
t.string "image_file_name"
t.string "image_content_type"
- t.bigint "image_file_size"
+ t.integer "image_file_size"
t.datetime "image_updated_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -157,10 +157,10 @@ ActiveRecord::Schema.define(version: 2019_08_04_115634) do
t.string "actor_type"
t.boolean "discoverable"
t.string "also_known_as", array: true
- t.datetime "silenced_at"
- t.datetime "suspended_at"
t.boolean "is_pro", default: false, null: false
t.datetime "pro_expires_at"
+ t.datetime "silenced_at"
+ t.datetime "suspended_at"
t.boolean "is_verified", default: false, null: false
t.boolean "is_donor", default: false, null: false
t.boolean "is_investor", default: false, null: false
@@ -578,6 +578,15 @@ ActiveRecord::Schema.define(version: 2019_08_04_115634) do
t.index ["status_id", "preview_card_id"], name: "index_preview_cards_statuses_on_status_id_and_preview_card_id"
end
+ create_table "promotions", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.datetime "expires_at"
+ t.bigint "status_id", null: false
+ t.string "timeline_id"
+ t.integer "position", default: 10
+ end
+
create_table "relays", force: :cascade do |t|
t.string "inbox_url", default: "", null: false
t.string "follow_activity_id"
@@ -658,8 +667,8 @@ ActiveRecord::Schema.define(version: 2019_08_04_115634) do
create_table "status_pins", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "status_id", null: false
- t.datetime "created_at", default: -> { "now()" }, null: false
- t.datetime "updated_at", default: -> { "now()" }, null: false
+ t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }, 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
end
From 0499184b3898f1950684fbf6f4f0fe91e073eccd Mon Sep 17 00:00:00 2001
From: 2458773093 <2458773093@protonmail.com>
Date: Sat, 31 Aug 2019 09:15:32 +0300
Subject: [PATCH 2/5] added active promoted statuses to initial state
---
app/serializers/initial_state_serializer.rb | 7 ++++++-
app/serializers/rest/promotion_serializer.rb | 5 +++++
2 files changed, 11 insertions(+), 1 deletion(-)
create mode 100644 app/serializers/rest/promotion_serializer.rb
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 5226da10..84c26889 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -2,7 +2,8 @@
class InitialStateSerializer < ActiveModel::Serializer
attributes :meta, :compose, :accounts,
- :media_attachments, :settings
+ :media_attachments, :settings,
+ :promotions
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
@@ -65,6 +66,10 @@ class InitialStateSerializer < ActiveModel::Serializer
{ accept_content_types: MediaAttachment::IMAGE_FILE_EXTENSIONS + MediaAttachment::VIDEO_FILE_EXTENSIONS + MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES }
end
+ def promotions
+ ActiveModelSerializers::SerializableResource.new(Promotion.active, each_serializer: REST::PromotionSerializer)
+ end
+
private
def instance_presenter
diff --git a/app/serializers/rest/promotion_serializer.rb b/app/serializers/rest/promotion_serializer.rb
new file mode 100644
index 00000000..3661765c
--- /dev/null
+++ b/app/serializers/rest/promotion_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class REST::PromotionSerializer < ActiveModel::Serializer
+ attributes :status_id, :timeline_id, :position
+end
From 168642a1b99c7f85a83256088b5cbd5fcb7aad39 Mon Sep 17 00:00:00 2001
From: 2458773093 <2458773093@protonmail.com>
Date: Sat, 31 Aug 2019 14:54:03 +0300
Subject: [PATCH 3/5] promotion fe
---
app/javascript/gabsocial/components/status.js | 12 ++++-
.../gabsocial/components/status_list.js | 44 ++++++++++++++-----
.../ui/containers/status_list_container.js | 10 ++++-
app/javascript/gabsocial/initial_state.js | 1 +
4 files changed, 53 insertions(+), 14 deletions(-)
diff --git a/app/javascript/gabsocial/components/status.js b/app/javascript/gabsocial/components/status.js
index 715e887c..4e8fde9e 100644
--- a/app/javascript/gabsocial/components/status.js
+++ b/app/javascript/gabsocial/components/status.js
@@ -92,6 +92,7 @@ class Status extends ImmutablePureComponent {
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
group: ImmutablePropTypes.map,
+ promoted: PropTypes.bool
};
// Avoid checking props that are functions (and whose equality will always
@@ -261,7 +262,7 @@ class Status extends ImmutablePureComponent {
let media = null;
let statusAvatar, prepend, rebloggedByText, reblogContent;
- const { intl, hidden, featured, otherAccounts, unread, showThread, group } = this.props;
+ const { intl, hidden, featured, otherAccounts, unread, showThread, group, promoted } = this.props;
let { status, account, ...other } = this.props;
@@ -293,7 +294,14 @@ class Status extends ImmutablePureComponent {
);
}
- if (featured) {
+ if (promoted) {
+ prepend = (
+
+ );
+ } else if (featured) {
prepend = (
diff --git a/app/javascript/gabsocial/components/status_list.js b/app/javascript/gabsocial/components/status_list.js
index b1ed75d6..83dd405b 100644
--- a/app/javascript/gabsocial/components/status_list.js
+++ b/app/javascript/gabsocial/components/status_list.js
@@ -29,12 +29,24 @@ export default class StatusList extends ImmutablePureComponent {
withGroupAdmin: PropTypes.bool,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
+ promotion: PropTypes.object,
+ promotedStatus: ImmutablePropTypes.map,
+ fetchStatus: PropTypes.func,
};
componentDidMount() {
this.handleDequeueTimeline();
+ this.fetchPromotedStatus();
};
+ fetchPromotedStatus() {
+ const { promotion, promotedStatus, fetchStatus } = this.props;
+
+ if (promotion && !promotedStatus) {
+ fetchStatus(promotion.status_id);
+ }
+ }
+
getFeaturedStatusCount = () => {
return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
}
@@ -86,7 +98,7 @@ export default class StatusList extends ImmutablePureComponent {
}
render () {
- const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, ...other } = this.props;
+ const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, promotion, promotedStatus, ...other } = this.props;
if (isPartial) {
return (
@@ -110,16 +122,26 @@ export default class StatusList extends ImmutablePureComponent {
onClick={onLoadMore}
/>
) : (
-
+
+
+
+ {promotedStatus && index === promotion.position && (
+
+ )}
+
))
) : null;
diff --git a/app/javascript/gabsocial/features/ui/containers/status_list_container.js b/app/javascript/gabsocial/features/ui/containers/status_list_container.js
index 959b61a7..77c6b2e3 100644
--- a/app/javascript/gabsocial/features/ui/containers/status_list_container.js
+++ b/app/javascript/gabsocial/features/ui/containers/status_list_container.js
@@ -3,9 +3,11 @@ import StatusList from '../../../components/status_list';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { createSelector } from 'reselect';
import { debounce } from 'lodash';
-import { me } from '../../../initial_state';
+import { me, promotions } from '../../../initial_state';
import { dequeueTimeline } from 'gabsocial/actions/timelines';
import { scrollTopTimeline } from '../../../actions/timelines';
+import { sample } from 'lodash';
+import { fetchStatus } from '../../../actions/statuses';
const makeGetStatusIds = () => createSelector([
(state, { type, id }) => state.getIn(['settings', type], ImmutableMap()),
@@ -32,6 +34,7 @@ const makeGetStatusIds = () => createSelector([
const mapStateToProps = (state, {timelineId}) => {
const getStatusIds = makeGetStatusIds();
+ const promotion = promotions.length > 0 && sample(promotions.filter(p => p.timeline_id === timelineId));
return {
statusIds: getStatusIds(state, { type: timelineId.substring(0,5) === 'group' ? 'group' : timelineId, id: timelineId }),
@@ -39,6 +42,8 @@ const mapStateToProps = (state, {timelineId}) => {
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
totalQueuedItemsCount: state.getIn(['timelines', timelineId, 'totalQueuedItemsCount']),
+ promotion: promotion,
+ promotedStatus: promotion && state.getIn(['statuses', promotion.status_id])
};
};
@@ -52,6 +57,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
onScroll: debounce(() => {
dispatch(scrollTopTimeline(ownProps.timelineId, false));
}, 100),
+ fetchStatus(id) {
+ dispatch(fetchStatus(id));
+ }
});
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
diff --git a/app/javascript/gabsocial/initial_state.js b/app/javascript/gabsocial/initial_state.js
index 315abc4e..07107232 100644
--- a/app/javascript/gabsocial/initial_state.js
+++ b/app/javascript/gabsocial/initial_state.js
@@ -23,5 +23,6 @@ export const mascot = getMeta('mascot');
export const profile_directory = getMeta('profile_directory');
export const isStaff = getMeta('is_staff');
export const forceSingleColumn = !getMeta('advanced_layout');
+export const promotions = initialState && initialState.promotions;
export default initialState;
From daf5eeb90583bd1206302fb1585e8cfd48987a85 Mon Sep 17 00:00:00 2001
From: 2458773093 <2458773093@protonmail.com>
Date: Wed, 4 Sep 2019 00:54:18 +0300
Subject: [PATCH 4/5] convert status ids to string
---
app/serializers/rest/promotion_serializer.rb | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/app/serializers/rest/promotion_serializer.rb b/app/serializers/rest/promotion_serializer.rb
index 3661765c..ad844d39 100644
--- a/app/serializers/rest/promotion_serializer.rb
+++ b/app/serializers/rest/promotion_serializer.rb
@@ -2,4 +2,8 @@
class REST::PromotionSerializer < ActiveModel::Serializer
attributes :status_id, :timeline_id, :position
+
+ def status_id
+ object.status_id.to_s
+ end
end
From d853beebb7f216e82b61ad1216e913eeba5b7bc1 Mon Sep 17 00:00:00 2001
From: 2458773093 <2458773093@protonmail.com>
Date: Wed, 4 Sep 2019 01:29:46 +0300
Subject: [PATCH 5/5] admin for promotions
---
.../settings/promotions_controller.rb | 56 +++++++++++++++++++
.../settings/promotions/_promotion.html.haml | 8 +++
app/views/settings/promotions/edit.html.haml | 14 +++++
app/views/settings/promotions/index.html.haml | 16 ++++++
app/views/settings/promotions/new.html.haml | 14 +++++
config/locales/en.yml | 2 +
config/navigation.rb | 1 +
config/routes.rb | 4 +-
8 files changed, 114 insertions(+), 1 deletion(-)
create mode 100644 app/controllers/settings/promotions_controller.rb
create mode 100644 app/views/settings/promotions/_promotion.html.haml
create mode 100644 app/views/settings/promotions/edit.html.haml
create mode 100644 app/views/settings/promotions/index.html.haml
create mode 100644 app/views/settings/promotions/new.html.haml
diff --git a/app/controllers/settings/promotions_controller.rb b/app/controllers/settings/promotions_controller.rb
new file mode 100644
index 00000000..147e6131
--- /dev/null
+++ b/app/controllers/settings/promotions_controller.rb
@@ -0,0 +1,56 @@
+class Settings::PromotionsController < Admin::BaseController
+ before_action :set_promotion, except: [:index, :new, :create]
+
+ def index
+ @promotions = Promotion.all
+ end
+
+ def new
+ @promotion = Promotion.new
+ end
+
+ def create
+ @promotion = Promotion.new(resource_params)
+
+ if @promotion.save
+ log_action :create, @promotion
+ redirect_to settings_promotions_path, notice: I18n.t('promotions.created_msg')
+ else
+ render :new
+ end
+ end
+
+ def edit
+ end
+
+ def update
+ if @promotion.update(resource_params)
+ log_action :update, @promotion
+ flash[:notice] = I18n.t('promotions.updated_msg')
+ else
+ flash[:alert] = I18n.t('promotions.update_failed_msg')
+ end
+ redirect_to settings_promotions_path
+ end
+
+ def destroy
+ @promotion.destroy!
+ log_action :destroy, @promotion
+ flash[:notice] = I18n.t('promotions.destroyed_msg')
+ redirect_to settings_promotions_path
+ end
+
+ private
+
+ def set_promotion
+ @promotion = Promotion.find(params[:id])
+ end
+
+ def set_filter_params
+ @filter_params = filter_params.to_hash.symbolize_keys
+ end
+
+ def resource_params
+ params.require(:promotion).permit(:expires_at, :status_id, :timeline_id, :position)
+ end
+end
diff --git a/app/views/settings/promotions/_promotion.html.haml b/app/views/settings/promotions/_promotion.html.haml
new file mode 100644
index 00000000..73833555
--- /dev/null
+++ b/app/views/settings/promotions/_promotion.html.haml
@@ -0,0 +1,8 @@
+%tr
+ %td= promotion.timeline_id
+ %td= promotion.status_id
+ %td= promotion.expires_at
+ %td= promotion.position
+ %td
+ = table_link_to 'pencil', t('promotions.edit'), edit_settings_promotion_path(promotion)
+ = table_link_to 'trash', t('promotions.delete'), settings_promotion_path(promotion), method: :delete, data: { confirm: t('settings.promotions.are_you_sure') }
diff --git a/app/views/settings/promotions/edit.html.haml b/app/views/settings/promotions/edit.html.haml
new file mode 100644
index 00000000..ccfeb379
--- /dev/null
+++ b/app/views/settings/promotions/edit.html.haml
@@ -0,0 +1,14 @@
+- content_for :page_title do
+ = t('promotions.title')
+
+= simple_form_for @promotion, url: settings_promotion_path(@promotion) do |f|
+ = render 'shared/error_messages', object: @promotion
+
+ .fields-group
+ = f.input :timeline_id, wrapper: :with_label, label: t('promotions.timeline_id')
+ = f.input :status_id, wrapper: :with_label, label: t('promotions.status_id')
+ = f.input :expires_at, as: :string, wrapper: :with_label, label: t('promotions.expires_at')
+ = f.input :position, wrapper: :with_label, label: t('promotions.position')
+
+ .actions
+ = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/settings/promotions/index.html.haml b/app/views/settings/promotions/index.html.haml
new file mode 100644
index 00000000..5cbbec30
--- /dev/null
+++ b/app/views/settings/promotions/index.html.haml
@@ -0,0 +1,16 @@
+- content_for :page_title do
+ = t('promotions.title')
+
+.table-wrapper
+ %table.table
+ %thead
+ %tr
+ %th= t('promotions.timeline_id')
+ %th= t('promotions.status_id')
+ %th= t('promotions.expires_at')
+ %th= t('promotions.position')
+ %th
+ %tbody
+ = render @promotions
+
+= link_to t('promotions.create'), new_settings_promotion_path, class: 'button'
diff --git a/app/views/settings/promotions/new.html.haml b/app/views/settings/promotions/new.html.haml
new file mode 100644
index 00000000..8e73e308
--- /dev/null
+++ b/app/views/settings/promotions/new.html.haml
@@ -0,0 +1,14 @@
+- content_for :page_title do
+ = t('.title')
+
+= simple_form_for @promotion, url: settings_promotions_path do |f|
+ = render 'shared/error_messages', object: @promotion
+
+ .fields-group
+ = f.input :timeline_id, wrapper: :with_label, label: t('promotions.timeline_id')
+ = f.input :status_id, wrapper: :with_label, label: t('promotions.status_id')
+ = f.input :expires_at, as: :string, wrapper: :with_label, label: t('promotions.expires_at')
+ = f.input :position, wrapper: :with_label, label: t('promotions.position')
+
+ .actions
+ = f.button :button, t('.create'), type: :submit
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 41c657a4..427db50f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -70,6 +70,8 @@ en:
moderator: Mod
unavailable: Profile unavailable
unfollow: Unfollow
+ promotions:
+ title: Promotions
admin:
account_actions:
action: Perform action
diff --git a/config/navigation.rb b/config/navigation.rb
index 531a19b3..3282b6e5 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -55,6 +55,7 @@ SimpleNavigation::Configuration.run do |navigation|
s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
s.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
s.item :moderation, safe_join([fa_icon('id-card-o fw'), t('verifications.moderation.title')]), settings_verifications_moderation_url, if: -> { current_user.admin? }
+ s.item :promotions, safe_join([fa_icon('star fw'), t('promotions.title')]), settings_promotions_url, if: -> { current_user.admin? }
end
n.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' }
diff --git a/config/routes.rb b/config/routes.rb
index 27ca11dc..bae766bb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -89,6 +89,8 @@ Rails.application.routes.draw do
post '/btcpay-notification', to: 'upgrade#btcpay_notification', as: :btcpay_notification
end
+ resources :promotions, only: [:index, :new, :create, :edit, :update, :destroy]
+
namespace :verifications do
get :moderation, to: 'moderation#index', as: :moderation
get 'moderation/:id/approve', to: 'moderation#approve', as: :approve
@@ -236,7 +238,7 @@ Rails.application.routes.draw do
resources :users, only: [] do
resource :two_factor_authentication, only: [:destroy]
end
-
+
resources :custom_emojis, only: [:index, :new, :create, :update, :destroy] do
member do
post :copy