diff --git a/app/controllers/api/v1/favourite_domains_controller.rb b/app/controllers/api/v1/favourite_domains_controller.rb new file mode 100644 index 000000000..62db6f6c5 --- /dev/null +++ b/app/controllers/api/v1/favourite_domains_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class Api::V1::FavouriteDomainsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:favourite_domains' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :write, :'write:favourite_domains' }, except: [:index, :show] + + before_action :require_user! + before_action :set_favourite_domain, except: [:index, :create] + + def index + @favourite_domains = FavouriteDomain.where(account: current_account).all + render json: @favourite_domains, each_serializer: REST::FavouriteDomainSerializer + end + + def show + render json: @favourite_domain, serializer: REST::FavouriteDomainSerializer + end + + def create + @favourite_domain = FavouriteDomain.create!(favourite_domain_params.merge(account: current_account)) + render json: @favourite_domain, serializer: REST::FavouriteDomainSerializer + end + + def update + @favourite_domain.update!(favourite_domain_params) + render json: @favourite_domain, serializer: REST::FavouriteDomainSerializer + end + + def destroy + @favourite_domain.destroy! + render_empty + end + + private + + def set_favourite_domain + @favourite_domain = FavouriteDomain.where(account: current_account).find(params[:id]) + end + + def favourite_domain_params + params.permit(:name) + end +end diff --git a/app/controllers/settings/favourite_domains_controller.rb b/app/controllers/settings/favourite_domains_controller.rb new file mode 100644 index 000000000..f9de92504 --- /dev/null +++ b/app/controllers/settings/favourite_domains_controller.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class Settings::FavouriteDomainsController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :set_favourite_domains, only: :index + before_action :set_favourite_domain, except: [:index, :create] + + def index + @favourite_domain = FavouriteDomain.new + end + + def create + @favourite_domain = current_account.favourite_domains.new(favourite_domain_params) + + if @favourite_domain.save + redirect_to settings_favourite_domains_path + else + set_favourite_domains + + render :index + end + end + + def destroy + @favourite_domain.destroy! + redirect_to settings_favourite_domains_path + end + + private + + def set_favourite_domain + @favourite_domain = current_account.favourite_domains.find(params[:id]) + end + + def set_favourite_domains + @favourite_domains = current_account.favourite_domains.order(:updated_at).reject(&:new_record?) + end + + def favourite_domain_params + params.require(:favourite_domain).permit(:name) + end +end diff --git a/app/javascript/mastodon/actions/favourite_domains.js b/app/javascript/mastodon/actions/favourite_domains.js new file mode 100644 index 000000000..6304eb852 --- /dev/null +++ b/app/javascript/mastodon/actions/favourite_domains.js @@ -0,0 +1,59 @@ +import api from '../api'; + +export const FAVOURITE_DOMAIN_FETCH_REQUEST = 'FAVOURITE_DOMAIN_FETCH_REQUEST'; +export const FAVOURITE_DOMAIN_FETCH_SUCCESS = 'FAVOURITE_DOMAIN_FETCH_SUCCESS'; +export const FAVOURITE_DOMAIN_FETCH_FAIL = 'FAVOURITE_DOMAIN_FETCH_FAIL'; + +export const FAVOURITE_DOMAINS_FETCH_REQUEST = 'FAVOURITE_DOMAINS_FETCH_REQUEST'; +export const FAVOURITE_DOMAINS_FETCH_SUCCESS = 'FAVOURITE_DOMAINS_FETCH_SUCCESS'; +export const FAVOURITE_DOMAINS_FETCH_FAIL = 'FAVOURITE_DOMAINS_FETCH_FAIL'; + +export const fetchFavouriteDomain = id => (dispatch, getState) => { + if (getState().getIn(['favourite_domains', id])) { + return; + } + + dispatch(fetchFavouriteDomainRequest(id)); + + api(getState).get(`/api/v1/favourite_domains/${id}`) + .then(({ data }) => dispatch(fetchFavouriteDomainSuccess(data))) + .catch(err => dispatch(fetchFavouriteDomainFail(id, err))); +}; + +export const fetchFavouriteDomainRequest = id => ({ + type: FAVOURITE_DOMAIN_FETCH_REQUEST, + id, +}); + +export const fetchFavouriteDomainSuccess = favourite_domain => ({ + type: FAVOURITE_DOMAIN_FETCH_SUCCESS, + favourite_domain, +}); + +export const fetchFavouriteDomainFail = (id, error) => ({ + type: FAVOURITE_DOMAIN_FETCH_FAIL, + id, + error, +}); + +export const fetchFavouriteDomains = () => (dispatch, getState) => { + dispatch(fetchFavouriteDomainsRequest()); + + api(getState).get('/api/v1/favourite_domains') + .then(({ data }) => dispatch(fetchFavouriteDomainsSuccess(data))) + .catch(err => dispatch(fetchFavouriteDomainsFail(err))); +}; + +export const fetchFavouriteDomainsRequest = () => ({ + type: FAVOURITE_DOMAINS_FETCH_REQUEST, +}); + +export const fetchFavouriteDomainsSuccess = favourite_domains => ({ + type: FAVOURITE_DOMAINS_FETCH_SUCCESS, + favourite_domains, +}); + +export const fetchFavouriteDomainsFail = error => ({ + type: FAVOURITE_DOMAINS_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/features/ui/components/favourite_domain_panel.js b/app/javascript/mastodon/features/ui/components/favourite_domain_panel.js new file mode 100644 index 000000000..a54a3d756 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/favourite_domain_panel.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { fetchFavouriteDomains } from 'mastodon/actions/favourite_domains'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { NavLink, withRouter } from 'react-router-dom'; +import Icon from 'mastodon/components/icon'; + +const getOrderedDomains = createSelector([state => state.get('favourite_domains')], favourite_domains => { + if (!favourite_domains) { + return favourite_domains; + } + + return favourite_domains.toList().filter(item => !!item).sort((a, b) => a.get('updated_at').localeCompare(b.get('updated_at'))).take(10); +}); + +const mapStateToProps = state => ({ + favourite_domains: getOrderedDomains(state), +}); + +export default @withRouter +@connect(mapStateToProps) +class FavouriteDomainPanel extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + favourite_domains: ImmutablePropTypes.list, + }; + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchFavouriteDomains()); + } + + render () { + const { favourite_domains } = this.props; + + if (!favourite_domains || favourite_domains.isEmpty()) { + return null; + } + + return ( +
+
+ + {favourite_domains.map(favourite_domain => ( + {favourite_domain.get('name')} + ))} +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index cc29f6995..d54c0cb39 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -6,6 +6,7 @@ import { profile_directory, showTrends } from 'mastodon/initial_state'; import NotificationsCounterIcon from './notifications_counter_icon'; import FollowRequestsNavLink from './follow_requests_nav_link'; import ListPanel from './list_panel'; +import FavouriteDomainPanel from './favourite_domain_panel'; import FavouriteTagPanel from './favourite_tag_panel'; import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container'; @@ -22,6 +23,7 @@ const NavigationPanel = () => ( {profile_directory && } +
diff --git a/app/javascript/mastodon/reducers/favourite_domains.js b/app/javascript/mastodon/reducers/favourite_domains.js new file mode 100644 index 000000000..3fd0e641c --- /dev/null +++ b/app/javascript/mastodon/reducers/favourite_domains.js @@ -0,0 +1,31 @@ +import { + FAVOURITE_DOMAIN_FETCH_SUCCESS, + FAVOURITE_DOMAIN_FETCH_FAIL, + FAVOURITE_DOMAINS_FETCH_SUCCESS, +} from '../actions/favourite_domains'; +import { Map as ImmutableMap, fromJS } from 'immutable'; + +const initialState = ImmutableMap(); + +const normalizeFavouriteDomain = (state, favourite_domain) => state.set(favourite_domain.id, fromJS(favourite_domain)); + +const normalizeFavouriteDomains = (state, favourite_domains) => { + favourite_domains.forEach(favourite_domain => { + state = normalizeFavouriteDomain(state, favourite_domain); + }); + + return state; +}; + +export default function favourite_domains(state = initialState, action) { + switch(action.type) { + case FAVOURITE_DOMAIN_FETCH_SUCCESS: + return normalizeFavouriteDomain(state, action.favourite_domain); + case FAVOURITE_DOMAINS_FETCH_SUCCESS: + return normalizeFavouriteDomains(state, action.favourite_domains); + case FAVOURITE_DOMAIN_FETCH_FAIL: + return state.set(action.id, false); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 0629fff94..3e6fc96af 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -38,6 +38,7 @@ import missed_updates from './missed_updates'; import announcements from './announcements'; import markers from './markers'; import picture_in_picture from './picture_in_picture'; +import favourite_domains from './favourite_domains'; import favourite_tags from './favourite_tags'; const reducers = { @@ -80,6 +81,7 @@ const reducers = { missed_updates, markers, picture_in_picture, + favourite_domains, favourite_tags, }; diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 3eabc9724..699145115 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -57,6 +57,9 @@ module AccountAssociations has_many :migrations, class_name: 'AccountMigration', dependent: :destroy, inverse_of: :account has_many :aliases, class_name: 'AccountAlias', dependent: :destroy, inverse_of: :account + # Domains + has_many :favourite_domains, inverse_of: :account, dependent: :destroy + # Hashtags has_and_belongs_to_many :tags has_many :favourite_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account diff --git a/app/models/favourite_domain.rb b/app/models/favourite_domain.rb new file mode 100644 index 000000000..7af0e8843 --- /dev/null +++ b/app/models/favourite_domain.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: favourite_domains +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +class FavouriteDomain < ApplicationRecord + belongs_to :account, inverse_of: :favourite_domains, required: true + + validates :name, presence: true, on: :create + validate :validate_favourite_domains_limit, on: :create + + private + + def validate_favourite_domains_limit + errors.add(:base, I18n.t('favourite_domains.errors.limit')) if account.favourite_domains.count >= 10 + end +end diff --git a/app/serializers/rest/favourite_domain_serializer.rb b/app/serializers/rest/favourite_domain_serializer.rb new file mode 100644 index 000000000..f0c171206 --- /dev/null +++ b/app/serializers/rest/favourite_domain_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::FavouriteDomainSerializer < ActiveModel::Serializer + attributes :id, :name, :updated_at + + def id + object.id.to_s + end +end diff --git a/app/views/settings/favourite_domains/index.html.haml b/app/views/settings/favourite_domains/index.html.haml new file mode 100644 index 000000000..e6e16a045 --- /dev/null +++ b/app/views/settings/favourite_domains/index.html.haml @@ -0,0 +1,26 @@ +- content_for :page_title do + = t('settings.favourite_domains') + +%p= t('favourite_domains.hint_html') + +%hr.spacer/ + += simple_form_for @favourite_domain, url: settings_favourite_domains_path do |f| + = render 'shared/error_messages', object: @favourite_domain + + .fields-group + = f.input :name, wrapper: :with_block_label, hint: false + + .actions + = f.button :button, t('favourite_domains.add_new'), type: :submit + +%hr.spacer/ + +- @favourite_domains.each do |favourite_domain| + .directory__domain{ class: params[:domain] == favourite_domain.name ? 'active' : nil } + %div + %h4 + = fa_icon 'users' + = favourite_domain.name + %small + = table_link_to 'trash', t('filters.index.delete'), settings_favourite_domain_path(favourite_domain), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } diff --git a/config/locales/en.yml b/config/locales/en.yml index e2ef94071..02b8c63e1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -944,6 +944,11 @@ en: lists: Lists mutes: You mute storage: Media storage + favourite_domains: + add_new: Add new + errors: + limit: You have already favourite the maximum amount of domains + hint_html: "What are favourite domains? They are used only for you, and are tools to help you browse using domains. You can quickly switch between timelines." favourite_tags: add_new: Add new errors: @@ -1318,6 +1323,7 @@ en: domain_subscribes: Domain subscribes edit_profile: Edit profile export: Data export + favourite_domains: Favourite domains favourite_tags: Favourite hashtags featured_tags: Featured hashtags follow_and_subscriptions: Follows and subscriptions diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 0c9560435..e5120087f 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -905,6 +905,11 @@ ja: lists: リスト mutes: ミュート storage: メディア + favourite_domains: + add_new: 追加 + errors: + limit: お気に入りドメインの上限に達しました + hint_html: "お気に入りのドメインとは何ですか? それらはあなた自身のためにだけ使用される、ドメインを活用したブラウジングを助けるためのツールです。すばやくタイムラインを切り替えることができます。" favourite_tags: add_new: 追加 errors: @@ -1262,6 +1267,7 @@ ja: domain_subscribes: ドメインの購読 edit_profile: プロフィールを編集 export: データのエクスポート + favourite_domains: お気に入りドメイン favourite_tags: お気に入りハッシュタグ featured_tags: 注目のハッシュタグ follow_and_subscriptions: フォロー・購読 diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index e8c83a275..2bc0836e1 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -214,6 +214,8 @@ ja: timeline: タイムライン email_domain_block: with_dns_records: ドメインのMXレコードとIPアドレスを含む + favourite_domain: + name: ドメイン favourite_tag: name: ハッシュタグ featured_tag: diff --git a/config/navigation.rb b/config/navigation.rb index b833d68ad..007ca6eb6 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -13,6 +13,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_url, if: -> { current_user.functional? } do |s| s.item :appearance, safe_join([fa_icon('desktop fw'), t('settings.appearance')]), settings_preferences_appearance_url s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_url + s.item :favourite_domains, safe_join([fa_icon('users fw'), t('settings.favourite_domains')]), settings_favourite_domains_url s.item :favourite_tags, safe_join([fa_icon('hashtag fw'), t('settings.favourite_tags')]), settings_favourite_tags_url s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_url end diff --git a/config/routes.rb b/config/routes.rb index 24feab6d2..0d30cb94c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -164,6 +164,7 @@ Rails.application.routes.draw do resources :aliases, only: [:index, :create, :destroy] resources :sessions, only: [:destroy] resources :featured_tags, only: [:index, :create, :destroy] + resources :favourite_domains, only: [:index, :create, :destroy] resources :favourite_tags, only: [:index, :create, :destroy] resources :follow_tags, except: [:show] resources :account_subscribes, except: [:show] @@ -488,6 +489,7 @@ Rails.application.routes.draw do end resources :featured_tags, only: [:index, :create, :destroy] + resources :favourite_domains, only: [:index, :create, :show, :update, :destroy] resources :favourite_tags, only: [:index, :create, :show, :update, :destroy] resources :follow_tags, only: [:index, :create, :show, :update, :destroy] resources :domain_subscribes, only: [:index, :create, :show, :update, :destroy] diff --git a/db/migrate/20200705092100_create_favourite_domains.rb b/db/migrate/20200705092100_create_favourite_domains.rb new file mode 100644 index 000000000..9951087d0 --- /dev/null +++ b/db/migrate/20200705092100_create_favourite_domains.rb @@ -0,0 +1,10 @@ +class CreateFavouriteDomains < ActiveRecord::Migration[5.2] + def change + create_table :favourite_domains do |t| + t.references :account, foreign_key: { on_delete: :cascade } + t.string :name, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 71479b5b0..86a092799 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -442,6 +442,14 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do t.index ["from_account_id"], name: "index_encrypted_messages_on_from_account_id" end + create_table "favourite_domains", force: :cascade do |t| + t.bigint "account_id" + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_favourite_domains_on_account_id" + end + create_table "favourite_tags", force: :cascade do |t| t.bigint "account_id" t.bigint "tag_id" @@ -1120,6 +1128,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade add_foreign_key "encrypted_messages", "accounts", column: "from_account_id", on_delete: :cascade add_foreign_key "encrypted_messages", "devices", on_delete: :cascade + add_foreign_key "favourite_domains", "accounts", on_delete: :cascade add_foreign_key "favourite_tags", "accounts", on_delete: :cascade add_foreign_key "favourite_tags", "tags", on_delete: :cascade add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade diff --git a/spec/fabricators/favourite_domain_fabricator.rb b/spec/fabricators/favourite_domain_fabricator.rb new file mode 100644 index 000000000..9cb7be4f9 --- /dev/null +++ b/spec/fabricators/favourite_domain_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:favourite_domain) do + account + name +end diff --git a/spec/models/favourite_domain_spec.rb b/spec/models/favourite_domain_spec.rb new file mode 100644 index 000000000..a5da4da27 --- /dev/null +++ b/spec/models/favourite_domain_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe FavouriteDomain, type: :model do +end