Add favorite hashtags
This commit is contained in:
parent
3ae7effb6d
commit
fd5b7c14fa
21 changed files with 350 additions and 0 deletions
43
app/controllers/api/v1/favourite_tags_controller.rb
Normal file
43
app/controllers/api/v1/favourite_tags_controller.rb
Normal file
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::FavouriteTagsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:favourite_tags' }, only: [:index, :show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:favourite_tags' }, except: [:index, :show]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_favourite_tag, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@favourite_tags = FavouriteTag.where(account: current_account).all
|
||||
render json: @favourite_tags, each_serializer: REST::FavouriteTagSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @favourite_tag, serializer: REST::FavouriteTagSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
@favourite_tag = FavouriteTag.create!(favourite_tag_params.merge(account: current_account))
|
||||
render json: @favourite_tag, serializer: REST::FavouriteTagSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
@favourite_tag.update!(favourite_tag_params)
|
||||
render json: @favourite_tag, serializer: REST::FavouriteTagSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@favourite_tag.destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_favourite_tag
|
||||
@favourite_tag = FavouriteTag.where(account: current_account).find(params[:id])
|
||||
end
|
||||
|
||||
def favourite_tag_params
|
||||
params.permit(:name)
|
||||
end
|
||||
end
|
44
app/controllers/settings/favourite_tags_controller.rb
Normal file
44
app/controllers/settings/favourite_tags_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::FavouriteTagsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_favourite_tags, only: :index
|
||||
before_action :set_favourite_tag, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@favourite_tag = FavouriteTag.new
|
||||
end
|
||||
|
||||
def create
|
||||
@favourite_tag = current_account.favourite_tags.new(favourite_tag_params)
|
||||
|
||||
if @favourite_tag.save
|
||||
redirect_to settings_favourite_tags_path
|
||||
else
|
||||
set_favourite_tags
|
||||
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@favourite_tag.destroy!
|
||||
redirect_to settings_favourite_tags_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_favourite_tag
|
||||
@favourite_tag = current_account.favourite_tags.find(params[:id])
|
||||
end
|
||||
|
||||
def set_favourite_tags
|
||||
@favourite_tags = current_account.favourite_tags.order(:updated_at).reject(&:new_record?)
|
||||
end
|
||||
|
||||
def favourite_tag_params
|
||||
params.require(:favourite_tag).permit(:name)
|
||||
end
|
||||
end
|
59
app/javascript/mastodon/actions/favourite_tags.js
Normal file
59
app/javascript/mastodon/actions/favourite_tags.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import api from '../api';
|
||||
|
||||
export const FAVOURITE_TAG_FETCH_REQUEST = 'FAVOURITE_TAG_FETCH_REQUEST';
|
||||
export const FAVOURITE_TAG_FETCH_SUCCESS = 'FAVOURITE_TAG_FETCH_SUCCESS';
|
||||
export const FAVOURITE_TAG_FETCH_FAIL = 'FAVOURITE_TAG_FETCH_FAIL';
|
||||
|
||||
export const FAVOURITE_TAGS_FETCH_REQUEST = 'FAVOURITE_TAGS_FETCH_REQUEST';
|
||||
export const FAVOURITE_TAGS_FETCH_SUCCESS = 'FAVOURITE_TAGS_FETCH_SUCCESS';
|
||||
export const FAVOURITE_TAGS_FETCH_FAIL = 'FAVOURITE_TAGS_FETCH_FAIL';
|
||||
|
||||
export const fetchFavouriteTag = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['favourite_tags', id])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchFavouriteTagRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/favourite_tags/${id}`)
|
||||
.then(({ data }) => dispatch(fetchFavouriteTagSuccess(data)))
|
||||
.catch(err => dispatch(fetchFavouriteTagFail(id, err)));
|
||||
};
|
||||
|
||||
export const fetchFavouriteTagRequest = id => ({
|
||||
type: FAVOURITE_TAG_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchFavouriteTagSuccess = favourite_tag => ({
|
||||
type: FAVOURITE_TAG_FETCH_SUCCESS,
|
||||
favourite_tag,
|
||||
});
|
||||
|
||||
export const fetchFavouriteTagFail = (id, error) => ({
|
||||
type: FAVOURITE_TAG_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchFavouriteTags = () => (dispatch, getState) => {
|
||||
dispatch(fetchFavouriteTagsRequest());
|
||||
|
||||
api(getState).get('/api/v1/favourite_tags')
|
||||
.then(({ data }) => dispatch(fetchFavouriteTagsSuccess(data)))
|
||||
.catch(err => dispatch(fetchFavouriteTagsFail(err)));
|
||||
};
|
||||
|
||||
export const fetchFavouriteTagsRequest = () => ({
|
||||
type: FAVOURITE_TAGS_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchFavouriteTagsSuccess = favourite_tags => ({
|
||||
type: FAVOURITE_TAGS_FETCH_SUCCESS,
|
||||
favourite_tags,
|
||||
});
|
||||
|
||||
export const fetchFavouriteTagsFail = error => ({
|
||||
type: FAVOURITE_TAGS_FETCH_FAIL,
|
||||
error,
|
||||
});
|
|
@ -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 { fetchFavouriteTags } from 'mastodon/actions/favourite_tags';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const getOrderedTags = createSelector([state => state.get('favourite_tags')], favourite_tags => {
|
||||
if (!favourite_tags) {
|
||||
return favourite_tags;
|
||||
}
|
||||
|
||||
return favourite_tags.toList().filter(item => !!item).sort((a, b) => a.get('updated_at').localeCompare(b.get('updated_at'))).take(10);
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
favourite_tags: getOrderedTags(state),
|
||||
});
|
||||
|
||||
export default @withRouter
|
||||
@connect(mapStateToProps)
|
||||
class FavouriteTagPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
favourite_tags: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchFavouriteTags());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { favourite_tags } = this.props;
|
||||
|
||||
if (!favourite_tags || favourite_tags.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<hr />
|
||||
|
||||
{favourite_tags.map(favourite_tag => (
|
||||
<NavLink key={favourite_tag.get('id')} className='column-link column-link--transparent' strict to={`/timelines/tag/${favourite_tag.get('name')}`}><Icon className='column-link__icon' id='hashtag' fixedWidth />{favourite_tag.get('name')}</NavLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 FavouriteTagPanel from './favourite_tag_panel';
|
||||
import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
|
||||
|
||||
const NavigationPanel = () => (
|
||||
|
@ -22,6 +23,7 @@ const NavigationPanel = () => (
|
|||
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
|
||||
|
||||
<ListPanel />
|
||||
<FavouriteTagPanel />
|
||||
|
||||
<hr />
|
||||
|
||||
|
|
31
app/javascript/mastodon/reducers/favourite_tags.js
Normal file
31
app/javascript/mastodon/reducers/favourite_tags.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {
|
||||
FAVOURITE_TAG_FETCH_SUCCESS,
|
||||
FAVOURITE_TAG_FETCH_FAIL,
|
||||
FAVOURITE_TAGS_FETCH_SUCCESS,
|
||||
} from '../actions/favourite_tags';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
const normalizeFavouriteTag = (state, favourite_tag) => state.set(favourite_tag.id, fromJS(favourite_tag));
|
||||
|
||||
const normalizeFavouriteTags = (state, favourite_tags) => {
|
||||
favourite_tags.forEach(favourite_tag => {
|
||||
state = normalizeFavouriteTag(state, favourite_tag);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default function favourite_tags(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case FAVOURITE_TAG_FETCH_SUCCESS:
|
||||
return normalizeFavouriteTag(state, action.favourite_tag);
|
||||
case FAVOURITE_TAGS_FETCH_SUCCESS:
|
||||
return normalizeFavouriteTags(state, action.favourite_tags);
|
||||
case FAVOURITE_TAG_FETCH_FAIL:
|
||||
return state.set(action.id, false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -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_tags from './favourite_tags';
|
||||
|
||||
const reducers = {
|
||||
announcements,
|
||||
|
@ -79,6 +80,7 @@ const reducers = {
|
|||
missed_updates,
|
||||
markers,
|
||||
picture_in_picture,
|
||||
favourite_tags,
|
||||
};
|
||||
|
||||
export default combineReducers(reducers);
|
||||
|
|
|
@ -59,6 +59,7 @@ module AccountAssociations
|
|||
|
||||
# Hashtags
|
||||
has_and_belongs_to_many :tags
|
||||
has_many :favourite_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
||||
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Account deletion requests
|
||||
|
|
32
app/models/favourite_tag.rb
Normal file
32
app/models/favourite_tag.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: favourite_tags
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# tag_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class FavouriteTag < ApplicationRecord
|
||||
belongs_to :account, inverse_of: :favourite_tags, required: true
|
||||
belongs_to :tag, inverse_of: :favourite_tags, required: true
|
||||
|
||||
delegate :name, to: :tag, allow_nil: true
|
||||
|
||||
validates_associated :tag, on: :create
|
||||
validates :name, presence: true, on: :create
|
||||
validate :validate_favourite_tags_limit, on: :create
|
||||
|
||||
def name=(str)
|
||||
self.tag = Tag.find_or_create_by_names(str.strip)&.first
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_favourite_tags_limit
|
||||
errors.add(:base, I18n.t('favourite_tags.errors.limit')) if account.favourite_tags.count >= 10
|
||||
end
|
||||
end
|
|
@ -21,6 +21,7 @@ class Tag < ApplicationRecord
|
|||
has_and_belongs_to_many :statuses
|
||||
has_and_belongs_to_many :accounts
|
||||
|
||||
has_many :favourite_tags, dependent: :destroy, inverse_of: :tag
|
||||
has_many :featured_tags, dependent: :destroy, inverse_of: :tag
|
||||
|
||||
HASHTAG_SEPARATORS = "_\u00B7\u200c"
|
||||
|
|
9
app/serializers/rest/favourite_tag_serializer.rb
Normal file
9
app/serializers/rest/favourite_tag_serializer.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::FavouriteTagSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :updated_at
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
end
|
26
app/views/settings/favourite_tags/index.html.haml
Normal file
26
app/views/settings/favourite_tags/index.html.haml
Normal file
|
@ -0,0 +1,26 @@
|
|||
- content_for :page_title do
|
||||
= t('settings.favourite_tags')
|
||||
|
||||
%p= t('favourite_tags.hint_html')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= simple_form_for @favourite_tag, url: settings_favourite_tags_path do |f|
|
||||
= render 'shared/error_messages', object: @favourite_tag
|
||||
|
||||
.fields-group
|
||||
= f.input :name, wrapper: :with_block_label, hint: false
|
||||
|
||||
.actions
|
||||
= f.button :button, t('favourite_tags.add_new'), type: :submit
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- @favourite_tags.each do |favourite_tag|
|
||||
.directory__tag{ class: params[:tag] == favourite_tag.name ? 'active' : nil }
|
||||
%div
|
||||
%h4
|
||||
= fa_icon 'hashtag'
|
||||
= favourite_tag.name
|
||||
%small
|
||||
= table_link_to 'trash', t('filters.index.delete'), settings_favourite_tag_path(favourite_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
|
@ -911,6 +911,11 @@ en:
|
|||
lists: Lists
|
||||
mutes: You mute
|
||||
storage: Media storage
|
||||
favourite_tags:
|
||||
add_new: Add new
|
||||
errors:
|
||||
limit: You have already favourite the maximum amount of hashtags
|
||||
hint_html: "<strong>What are favourite hashtags?</strong> They are used only for you, and are tools to help you browse using hashtags. You can quickly switch between timelines."
|
||||
featured_tags:
|
||||
add_new: Add new
|
||||
errors:
|
||||
|
@ -1245,6 +1250,7 @@ en:
|
|||
development: Development
|
||||
edit_profile: Edit profile
|
||||
export: Data export
|
||||
favourite_tags: Favourite hashtags
|
||||
featured_tags: Featured hashtags
|
||||
identity_proofs: Identity proofs
|
||||
import: Import
|
||||
|
|
|
@ -889,6 +889,11 @@ ja:
|
|||
lists: リスト
|
||||
mutes: ミュート
|
||||
storage: メディア
|
||||
favourite_tags:
|
||||
add_new: 追加
|
||||
errors:
|
||||
limit: お気に入りハッシュタグの上限に達しました
|
||||
hint_html: "<strong>お気に入りのハッシュタグとは何ですか?</strong> それらはあなた自身のためにだけ使用される、ハッシュタグを活用したブラウジングを助けるためのツールです。すばやくタイムラインを切り替えることができます。"
|
||||
featured_tags:
|
||||
add_new: 追加
|
||||
errors:
|
||||
|
@ -1206,6 +1211,7 @@ ja:
|
|||
development: 開発
|
||||
edit_profile: プロフィールを編集
|
||||
export: データのエクスポート
|
||||
favourite_tags: お気に入りハッシュタグ
|
||||
featured_tags: 注目のハッシュタグ
|
||||
identity_proofs: Identity proofs
|
||||
import: データのインポート
|
||||
|
|
|
@ -178,6 +178,8 @@ ja:
|
|||
whole_word: 単語全体にマッチ
|
||||
email_domain_block:
|
||||
with_dns_records: ドメインのMXレコードとIPアドレスを含む
|
||||
favourite_tag:
|
||||
name: ハッシュタグ
|
||||
featured_tag:
|
||||
name: ハッシュタグ
|
||||
interactions:
|
||||
|
|
|
@ -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_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
|
||||
|
||||
|
|
|
@ -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_tags, only: [:index, :create, :destroy]
|
||||
resources :login_activities, only: [:index]
|
||||
end
|
||||
|
||||
|
|
10
db/migrate/20190821124329_create_favourite_tags.rb
Normal file
10
db/migrate/20190821124329_create_favourite_tags.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
class CreateFavouriteTags < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :favourite_tags do |t|
|
||||
t.references :account, foreign_key: { on_delete: :cascade }
|
||||
t.references :tag, foreign_key: { on_delete: :cascade }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
11
db/schema.rb
11
db/schema.rb
|
@ -397,6 +397,15 @@ 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_tags", force: :cascade do |t|
|
||||
t.bigint "account_id"
|
||||
t.bigint "tag_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_favourite_tags_on_account_id"
|
||||
t.index ["tag_id"], name: "index_favourite_tags_on_tag_id"
|
||||
end
|
||||
|
||||
create_table "favourites", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
|
@ -1027,6 +1036,8 @@ 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_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
|
||||
add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
|
||||
add_foreign_key "featured_tags", "accounts", on_delete: :cascade
|
||||
|
|
4
spec/fabricators/favourite_tag_fabricator.rb
Normal file
4
spec/fabricators/favourite_tag_fabricator.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
Fabricator(:favourite_tag) do
|
||||
account
|
||||
tag
|
||||
end
|
4
spec/models/favourite_tag_spec.rb
Normal file
4
spec/models/favourite_tag_spec.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FavouriteTag, type: :model do
|
||||
end
|
Loading…
Reference in a new issue