2020-10-12 16:19:35 -07:00
import './public-path' ;
2018-12-07 07:42:22 -08:00
import escapeTextContentForBrowser from 'escape-html' ;
2017-05-30 06:11:15 -07:00
import loadPolyfills from '../mastodon/load_polyfills' ;
2017-09-09 07:23:44 -07:00
import ready from '../mastodon/ready' ;
2018-07-13 18:56:41 -07:00
import { start } from '../mastodon/common' ;
2019-11-04 04:03:09 -08:00
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions' ;
Revamp post filtering system (#18058)
* Add model for custom filter keywords
* Use CustomFilterKeyword internally
Does not change the API
* Fix /filters/edit and /filters/new
* Add migration tests
* Remove whole_word column from custom_filters (covered by custom_filter_keywords)
* Redesign /filters
Instead of a list, present a card that displays more information and handles
multiple keywords per filter.
* Redesign /filters/new and /filters/edit to add and remove keywords
This adds a new gem dependency: cocoon, as well as a npm dependency:
cocoon-js-vanilla. Those are used to easily populate and remove form fields
from the user interface when manipulating multiple keyword filters at once.
* Add /api/v2/filters to edit filter with multiple keywords
Entities:
- `Filter`: `id`, `title`, `filter_action` (either `hide` or `warn`), `context`
`keywords`
- `FilterKeyword`: `id`, `keyword`, `whole_word`
API endpoits:
- `GET /api/v2/filters` to list filters (including keywords)
- `POST /api/v2/filters` to create a new filter
`keywords_attributes` can also be passed to create keywords in one request
- `GET /api/v2/filters/:id` to read a particular filter
- `PUT /api/v2/filters/:id` to update a new filter
`keywords_attributes` can also be passed to edit, delete or add keywords in
one request
- `DELETE /api/v2/filters/:id` to delete a particular filter
- `GET /api/v2/filters/:id/keywords` to list keywords for a filter
- `POST /api/v2/filters/:filter_id/keywords/:id` to add a new keyword to a
filter
- `GET /api/v2/filter_keywords/:id` to read a particular keyword
- `PUT /api/v2/filter_keywords/:id` to edit a particular keyword
- `DELETE /api/v2/filter_keywords/:id` to delete a particular keyword
* Change from `irreversible` boolean to `action` enum
* Remove irrelevent `irreversible_must_be_within_context` check
* Fix /filters/new and /filters/edit with update for filter_action
* Fix Rubocop/Codeclimate complaining about task names
* Refactor FeedManager#phrase_filtered?
This moves regexp building and filter caching to the `CustomFilter` class.
This does not change the functional behavior yet, but this changes how the
cache is built, doing per-custom_filter regexps so that filters can be matched
independently, while still offering caching.
* Perform server-side filtering and output result in REST API
* Fix numerous filters_changed events being sent when editing multiple keywords at once
* Add some tests
* Use the new API in the WebUI
- use client-side logic for filters we have fetched rules for.
This is so that filter changes can be retroactively applied without
reloading the UI.
- use server-side logic for filters we haven't fetched rules for yet
(e.g. network error, or initial timeline loading)
* Minor optimizations and refactoring
* Perform server-side filtering on the streaming server
* Change the wording of filter action labels
* Fix issues pointed out by linter
* Change design of “Show anyway” link in accordence to review comments
* Drop “irreversible” filtering behavior
* Move /api/v2/filter_keywords to /api/v1/filters/keywords
* Rename `filter_results` attribute to `filtered`
* Rename REST::LegacyFilterSerializer to REST::V1::FilterSerializer
* Fix systemChannelId value in streaming server
* Simplify code by removing client-side filtering code
The simplifcation comes at a cost though: filters aren't retroactively
applied anymore.
2022-06-28 00:42:13 -07:00
import 'cocoon-js-vanilla' ;
2018-07-13 18:56:41 -07:00
start ( ) ;
2017-09-09 07:23:44 -07:00
window . addEventListener ( 'message' , e => {
const data = e . data || { } ;
if ( ! window . parent || data . type !== 'setHeight' ) {
return ;
}
ready ( ( ) => {
window . parent . postMessage ( {
type : 'setHeight' ,
id : data . id ,
height : document . getElementsByTagName ( 'html' ) [ 0 ] . scrollHeight ,
} , '*' ) ;
} ) ;
} ) ;
2017-06-09 06:06:38 -07:00
function main ( ) {
2018-07-28 10:25:33 -07:00
const IntlMessageFormat = require ( 'intl-messageformat' ) . default ;
const { timeAgoString } = require ( '../mastodon/components/relative_timestamp' ) ;
2020-03-20 19:14:50 -07:00
const { delegate } = require ( '@rails/ujs' ) ;
2017-10-05 18:42:34 -07:00
const emojify = require ( '../mastodon/features/emoji/emoji' ) . default ;
2017-07-17 15:19:02 -07:00
const { getLocale } = require ( '../mastodon/locales' ) ;
2018-07-28 10:25:33 -07:00
const { messages } = getLocale ( ) ;
2017-09-13 18:39:10 -07:00
const React = require ( 'react' ) ;
const ReactDOM = require ( 'react-dom' ) ;
2018-07-28 10:25:33 -07:00
const Rellax = require ( 'rellax' ) ;
2019-09-18 06:41:50 -07:00
const { createBrowserHistory } = require ( 'history' ) ;
2017-07-17 15:19:02 -07:00
2019-01-10 06:13:30 -08:00
const scrollToDetailedStatus = ( ) => {
2019-09-18 06:41:50 -07:00
const history = createBrowserHistory ( ) ;
2019-01-10 06:13:30 -08:00
const detailedStatuses = document . querySelectorAll ( '.public-layout .detailed-status' ) ;
const location = history . location ;
if ( detailedStatuses . length === 1 && ( ! location . state || ! location . state . scrolledToDetailedStatus ) ) {
detailedStatuses [ 0 ] . scrollIntoView ( ) ;
history . replace ( location . pathname , { ... location . state , scrolledToDetailedStatus : true } ) ;
}
} ;
2019-07-21 09:10:40 -07:00
const getEmojiAnimationHandler = ( swapTo ) => {
return ( { target } ) => {
target . src = target . getAttribute ( swapTo ) ;
} ;
} ;
2017-07-17 15:19:02 -07:00
ready ( ( ) => {
const locale = document . documentElement . lang ;
2017-09-09 07:23:44 -07:00
2017-07-17 15:19:02 -07:00
const dateTimeFormat = new Intl . DateTimeFormat ( locale , {
year : 'numeric' ,
month : 'long' ,
day : 'numeric' ,
hour : 'numeric' ,
minute : 'numeric' ,
} ) ;
2017-09-09 07:23:44 -07:00
2017-07-17 15:19:02 -07:00
[ ] . forEach . call ( document . querySelectorAll ( '.emojify' ) , ( content ) => {
content . innerHTML = emojify ( content . innerHTML ) ;
} ) ;
[ ] . forEach . call ( document . querySelectorAll ( 'time.formatted' ) , ( content ) => {
const datetime = new Date ( content . getAttribute ( 'datetime' ) ) ;
const formattedDate = dateTimeFormat . format ( datetime ) ;
2017-09-09 07:23:44 -07:00
2017-07-17 15:19:02 -07:00
content . title = formattedDate ;
content . textContent = formattedDate ;
} ) ;
[ ] . forEach . call ( document . querySelectorAll ( 'time.time-ago' ) , ( content ) => {
const datetime = new Date ( content . getAttribute ( 'datetime' ) ) ;
2018-07-28 10:25:33 -07:00
const now = new Date ( ) ;
2017-09-09 07:23:44 -07:00
2017-08-25 08:21:16 -07:00
content . title = dateTimeFormat . format ( datetime ) ;
2018-07-28 10:25:33 -07:00
content . textContent = timeAgoString ( {
formatMessage : ( { id , defaultMessage } , values ) => ( new IntlMessageFormat ( messages [ id ] || defaultMessage , locale ) ) . format ( values ) ,
formatDate : ( date , options ) => ( new Intl . DateTimeFormat ( locale , options ) ) . format ( date ) ,
2020-02-03 08:48:56 -08:00
} , datetime , now , now . getFullYear ( ) , content . getAttribute ( 'datetime' ) . includes ( 'T' ) ) ;
2017-07-17 15:19:02 -07:00
} ) ;
2017-08-30 01:23:43 -07:00
2018-05-12 06:30:06 -07:00
const reactComponents = document . querySelectorAll ( '[data-component]' ) ;
2018-09-18 07:45:58 -07:00
2018-05-12 06:30:06 -07:00
if ( reactComponents . length > 0 ) {
import ( /* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container' )
. then ( ( { default : MediaContainer } ) => {
2019-01-13 01:23:54 -08:00
[ ] . forEach . call ( reactComponents , ( component ) => {
[ ] . forEach . call ( component . children , ( child ) => {
component . removeChild ( child ) ;
} ) ;
} ) ;
2018-05-12 06:30:06 -07:00
const content = document . createElement ( 'div' ) ;
2018-03-24 04:52:26 -07:00
2018-05-12 06:30:06 -07:00
ReactDOM . render ( < MediaContainer locale = { locale } components = { reactComponents } / > , content ) ;
document . body . appendChild ( content ) ;
2019-01-10 06:13:30 -08:00
scrollToDetailedStatus ( ) ;
2018-05-12 06:30:06 -07:00
} )
2019-01-10 06:13:30 -08:00
. catch ( error => {
console . error ( error ) ;
scrollToDetailedStatus ( ) ;
} ) ;
} else {
scrollToDetailedStatus ( ) ;
2018-03-24 04:52:26 -07:00
}
2018-07-28 10:25:33 -07:00
2018-08-25 16:19:13 -07:00
const parallaxComponents = document . querySelectorAll ( '.parallax' ) ;
2018-09-18 07:45:58 -07:00
2018-08-25 16:19:13 -07:00
if ( parallaxComponents . length > 0 ) {
new Rellax ( '.parallax' , { speed : - 1 } ) ;
}
2019-07-21 09:10:40 -07:00
2020-08-11 14:09:13 -07:00
delegate ( document , '#registration_user_password_confirmation,#registration_user_password' , 'input' , ( ) => {
const password = document . getElementById ( 'registration_user_password' ) ;
const confirmation = document . getElementById ( 'registration_user_password_confirmation' ) ;
2021-12-05 12:49:50 -08:00
if ( confirmation . value && confirmation . value . length > password . maxLength ) {
confirmation . setCustomValidity ( ( new IntlMessageFormat ( messages [ 'password_confirmation.exceeds_maxlength' ] || 'Password confirmation exceeds the maximum password length' , locale ) ) . format ( ) ) ;
} else if ( password . value && password . value !== confirmation . value ) {
2020-08-11 14:09:13 -07:00
confirmation . setCustomValidity ( ( new IntlMessageFormat ( messages [ 'password_confirmation.mismatching' ] || 'Password confirmation does not match' , locale ) ) . format ( ) ) ;
} else {
confirmation . setCustomValidity ( '' ) ;
}
} ) ;
2020-08-12 03:11:15 -07:00
delegate ( document , '#user_password,#user_password_confirmation' , 'input' , ( ) => {
const password = document . getElementById ( 'user_password' ) ;
const confirmation = document . getElementById ( 'user_password_confirmation' ) ;
if ( ! confirmation ) return ;
2021-12-05 12:49:50 -08:00
if ( confirmation . value && confirmation . value . length > password . maxLength ) {
confirmation . setCustomValidity ( ( new IntlMessageFormat ( messages [ 'password_confirmation.exceeds_maxlength' ] || 'Password confirmation exceeds the maximum password length' , locale ) ) . format ( ) ) ;
} else if ( password . value && password . value !== confirmation . value ) {
2020-08-12 03:11:15 -07:00
confirmation . setCustomValidity ( ( new IntlMessageFormat ( messages [ 'password_confirmation.mismatching' ] || 'Password confirmation does not match' , locale ) ) . format ( ) ) ;
} else {
confirmation . setCustomValidity ( '' ) ;
}
} ) ;
2019-07-21 09:10:40 -07:00
delegate ( document , '.custom-emoji' , 'mouseover' , getEmojiAnimationHandler ( 'data-original' ) ) ;
delegate ( document , '.custom-emoji' , 'mouseout' , getEmojiAnimationHandler ( 'data-static' ) ) ;
2020-04-05 05:02:22 -07:00
delegate ( document , '.status__content__spoiler-link' , 'click' , function ( ) {
2020-04-28 01:16:55 -07:00
const statusEl = this . parentNode . parentNode ;
2020-04-05 05:02:22 -07:00
2020-04-28 01:16:55 -07:00
if ( statusEl . dataset . spoiler === 'expanded' ) {
statusEl . dataset . spoiler = 'folded' ;
2020-04-05 05:02:22 -07:00
this . textContent = ( new IntlMessageFormat ( messages [ 'status.show_more' ] || 'Show more' , locale ) ) . format ( ) ;
} else {
2020-04-28 01:16:55 -07:00
statusEl . dataset . spoiler = 'expanded' ;
2020-04-05 05:02:22 -07:00
this . textContent = ( new IntlMessageFormat ( messages [ 'status.show_less' ] || 'Show less' , locale ) ) . format ( ) ;
}
return false ;
} ) ;
[ ] . forEach . call ( document . querySelectorAll ( '.status__content__spoiler-link' ) , ( spoilerLink ) => {
2020-04-28 01:16:55 -07:00
const statusEl = spoilerLink . parentNode . parentNode ;
const message = ( statusEl . dataset . spoiler === 'expanded' ) ? ( messages [ 'status.show_less' ] || 'Show less' ) : ( messages [ 'status.show_more' ] || 'Show more' ) ;
2020-04-05 05:02:22 -07:00
spoilerLink . textContent = ( new IntlMessageFormat ( message , locale ) ) . format ( ) ;
} ) ;
2017-05-25 05:09:25 -07:00
} ) ;
2016-12-20 15:13:13 -08:00
2017-05-25 05:09:25 -07:00
delegate ( document , '.webapp-btn' , 'click' , ( { target , button } ) => {
if ( button !== 0 ) {
return true ;
}
window . location . href = target . href ;
return false ;
} ) ;
2017-03-31 04:54:36 -07:00
2018-08-17 18:03:12 -07:00
delegate ( document , '.modal-button' , 'click' , e => {
e . preventDefault ( ) ;
let href ;
if ( e . target . nodeName !== 'A' ) {
href = e . target . parentNode . href ;
} else {
href = e . target . href ;
}
window . open ( href , 'mastodon-intent' , 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes' ) ;
} ) ;
2018-07-28 10:25:33 -07:00
delegate ( document , '#account_display_name' , 'input' , ( { target } ) => {
2018-10-25 16:55:24 -07:00
const name = document . querySelector ( '.card .display-name strong' ) ;
2018-07-28 10:25:33 -07:00
if ( name ) {
2018-12-07 07:42:22 -08:00
if ( target . value ) {
name . innerHTML = emojify ( escapeTextContentForBrowser ( target . value ) ) ;
} else {
2020-04-28 01:16:55 -07:00
name . textContent = target . dataset . default ;
2018-12-07 07:42:22 -08:00
}
2018-07-28 10:25:33 -07:00
}
2017-05-25 05:09:25 -07:00
} ) ;
2017-05-02 17:04:16 -07:00
2017-07-21 03:47:16 -07:00
delegate ( document , '#account_avatar' , 'change' , ( { target } ) => {
2018-07-28 10:25:33 -07:00
const avatar = document . querySelector ( '.card .avatar img' ) ;
2017-07-21 03:47:16 -07:00
const [ file ] = target . files || [ ] ;
2017-09-11 07:19:54 -07:00
const url = file ? URL . createObjectURL ( file ) : avatar . dataset . originalSrc ;
2017-09-09 07:23:44 -07:00
2017-07-21 03:47:16 -07:00
avatar . src = url ;
} ) ;
2019-04-20 19:47:39 -07:00
const getProfileAvatarAnimationHandler = ( swapTo ) => {
//animate avatar gifs on the profile page when moused over
return ( { target } ) => {
const swapSrc = target . getAttribute ( swapTo ) ;
//only change the img source if autoplay is off and the image src is actually different
2019-07-19 00:18:23 -07:00
if ( target . getAttribute ( 'data-autoplay' ) !== 'true' && target . src !== swapSrc ) {
2019-04-20 19:47:39 -07:00
target . src = swapSrc ;
}
} ;
} ;
delegate ( document , 'img#profile_page_avatar' , 'mouseover' , getProfileAvatarAnimationHandler ( 'data-original' ) ) ;
delegate ( document , 'img#profile_page_avatar' , 'mouseout' , getProfileAvatarAnimationHandler ( 'data-static' ) ) ;
2017-07-21 03:47:16 -07:00
delegate ( document , '#account_header' , 'change' , ( { target } ) => {
2018-07-28 10:25:33 -07:00
const header = document . querySelector ( '.card .card__img img' ) ;
2017-07-21 03:47:16 -07:00
const [ file ] = target . files || [ ] ;
2017-09-11 07:19:54 -07:00
const url = file ? URL . createObjectURL ( file ) : header . dataset . originalSrc ;
2017-09-09 07:23:44 -07:00
2018-07-28 10:25:33 -07:00
header . src = url ;
} ) ;
delegate ( document , '#account_locked' , 'change' , ( { target } ) => {
const lock = document . querySelector ( '.card .display-name i' ) ;
2020-07-01 04:51:50 -07:00
if ( lock ) {
if ( target . checked ) {
delete lock . dataset . hidden ;
} else {
lock . dataset . hidden = 'true' ;
}
2018-07-28 10:25:33 -07:00
}
2017-07-21 03:47:16 -07:00
} ) ;
2018-09-18 07:45:58 -07:00
delegate ( document , '.input-copy input' , 'click' , ( { target } ) => {
2019-04-03 08:54:54 -07:00
target . focus ( ) ;
2018-09-18 07:45:58 -07:00
target . select ( ) ;
2019-04-03 08:54:54 -07:00
target . setSelectionRange ( 0 , target . value . length ) ;
2018-09-18 07:45:58 -07:00
} ) ;
delegate ( document , '.input-copy button' , 'click' , ( { target } ) => {
2018-10-09 10:36:13 -07:00
const input = target . parentNode . querySelector ( '.input-copy__wrapper input' ) ;
2018-09-18 07:45:58 -07:00
2019-04-03 08:54:54 -07:00
const oldReadOnly = input . readonly ;
input . readonly = false ;
2018-09-18 07:45:58 -07:00
input . focus ( ) ;
input . select ( ) ;
2019-04-03 08:54:54 -07:00
input . setSelectionRange ( 0 , input . value . length ) ;
2018-09-18 07:45:58 -07:00
try {
if ( document . execCommand ( 'copy' ) ) {
input . blur ( ) ;
target . parentNode . classList . add ( 'copied' ) ;
setTimeout ( ( ) => {
target . parentNode . classList . remove ( 'copied' ) ;
} , 700 ) ;
}
} catch ( err ) {
console . error ( err ) ;
}
2019-04-03 08:54:54 -07:00
input . readonly = oldReadOnly ;
2018-09-18 07:45:58 -07:00
} ) ;
2019-09-20 01:52:14 -07:00
delegate ( document , '.sidebar__toggle__icon' , 'click' , ( ) => {
2022-02-16 12:44:19 -08:00
document . querySelector ( '.sidebar ul' ) . classList . toggle ( 'visible' ) ;
2019-09-20 01:52:14 -07:00
} ) ;
2020-12-09 21:27:26 -08:00
// Empty the honeypot fields in JS in case something like an extension
// automatically filled them.
delegate ( document , '#registration_new_user,#new_user' , 'submit' , ( ) => {
[ 'user_website' , 'user_confirm_password' , 'registration_user_website' , 'registration_user_confirm_password' ] . forEach ( id => {
const field = document . getElementById ( id ) ;
if ( field ) {
field . value = '' ;
}
} ) ;
} ) ;
2017-05-25 05:09:25 -07:00
}
2017-05-20 09:15:43 -07:00
2019-11-04 04:03:09 -08:00
loadPolyfills ( )
. then ( main )
. then ( loadKeyboardExtensions )
. catch ( error => {
console . error ( error ) ;
} ) ;