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