From 20b1f8dcf6a4eb63bf78f6e4dd2df67e5eeaded1 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 29 Jun 2019 18:32:06 +0200 Subject: [PATCH 1/6] Use ScrollToOptions for smooth scrolling if supported (#11207) --- app/javascript/mastodon/scroll.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/scroll.js b/app/javascript/mastodon/scroll.js index 2af07e0fb1..84fe582699 100644 --- a/app/javascript/mastodon/scroll.js +++ b/app/javascript/mastodon/scroll.js @@ -26,5 +26,7 @@ const scroll = (node, key, target) => { }; }; -export const scrollRight = (node, position) => scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => scroll(node, 'scrollTop', 0); +const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; + +export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); +export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); From fa7866c9c815b9faa6296492fe8f9dd20650cda4 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 29 Jun 2019 18:32:36 +0200 Subject: [PATCH 2/6] When sending a toot, ensure a CW is only set if the CW field is visible (#11206) In some occasions, such as the browser or a browser extension auto-filling the existing but disabled/hidden CW field, a CW can be set without the user knowing. --- app/javascript/mastodon/actions/compose.js | 2 +- app/javascript/mastodon/reducers/compose.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 300fb48a99..fbf97d3747 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -139,7 +139,7 @@ export function submitCompose(routerHistory) { in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: media.map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']), - spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), + spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', visibility: getState().getIn(['compose', 'privacy']), poll: getState().getIn(['compose', 'poll'], null), }, { diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 29c6911440..8cdd29bfeb 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -224,6 +224,7 @@ export default function compose(state = initialState, action) { } }); case COMPOSE_SPOILER_TEXT_CHANGE: + if (!state.get('spoiler')) return state; return state .set('spoiler_text', action.text) .set('idempotencyKey', uuid()); From ff65c494a66e9153a5a24386f9b73b58c5ff7692 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 30 Jun 2019 00:12:38 +0200 Subject: [PATCH 3/6] Optimize makeGetStatus (#11211) * Optimize makeGetStatus Because `ImmutableList.filter` always returns a new object and `createSelector` memoizes based on object identity, the selector returned by `makeGetStatus` would *always* execute. To avoid that, we wrap `getFilters` into a new memoizer that memoizes based on deep equality, thus returning the same object as long as the filters haven't changed, allowing the memoization of `makeGetStatus` to work. Furthermore, we memoize the compiled regexs instead of recomputing them each time the selector is called. * Fix memoized result being cleared too often * Make notifications use memoized getFiltersRegex --- .../mastodon/actions/notifications.js | 8 ++--- app/javascript/mastodon/selectors/index.js | 35 ++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 88788eec9f..56c952cb05 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -11,7 +11,7 @@ import { saveSettings } from './settings'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; -import { getFilters, regexFromFilters } from '../selectors'; +import { getFiltersRegex } from '../selectors'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -43,13 +43,13 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); - const filters = getFilters(getState(), { contextType: 'notifications' }); + const filters = getFiltersRegex(getState(), { contextType: 'notifications' }); let filtered = false; if (notification.type === 'mention') { - const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible'))); - const regex = regexFromFilters(filters); + const dropRegex = filters[0]; + const regex = filters[1]; const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); if (dropRegex && dropRegex.test(searchIndex)) { diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index ff6c7fdfb3..c87654547d 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; +import { List as ImmutableList, is } from 'immutable'; import { me } from '../initial_state'; const getAccountBase = (state, id) => state.getIn(['accounts', id], null); @@ -36,12 +36,10 @@ const toServerSideType = columnType => { } }; -export const getFilters = (state, { contextType }) => state.get('filters', ImmutableList()).filter(filter => contextType && filter.get('context').includes(toServerSideType(contextType)) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); - const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -export const regexFromFilters = filters => { +const regexFromFilters = filters => { if (filters.size === 0) { return null; } @@ -63,6 +61,27 @@ export const regexFromFilters = filters => { }).join('|'), 'i'); }; +// Memoize the filter regexps for each valid server contextType +const makeGetFiltersRegex = () => { + let memo = {}; + + return (state, { contextType }) => { + if (!contextType) return ImmutableList(); + + const serverSideType = toServerSideType(contextType); + const filters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); + + if (!memo[serverSideType] || !is(memo[serverSideType].filters, filters)) { + const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible'))); + const regex = regexFromFilters(filters); + memo[serverSideType] = { filters: filters, results: [dropRegex, regex] }; + } + return memo[serverSideType].results; + }; +}; + +export const getFiltersRegex = makeGetFiltersRegex(); + export const makeGetStatus = () => { return createSelector( [ @@ -70,10 +89,10 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), - getFilters, + getFiltersRegex, ], - (statusBase, statusReblog, accountBase, accountReblog, filters) => { + (statusBase, statusReblog, accountBase, accountReblog, filtersRegex) => { if (!statusBase) { return null; } @@ -84,12 +103,12 @@ export const makeGetStatus = () => { statusReblog = null; } - const dropRegex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters.filter(filter => filter.get('irreversible'))); + const dropRegex = (accountReblog || accountBase).get('id') !== me && filtersRegex[0]; if (dropRegex && dropRegex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'))) { return null; } - const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters); + const regex = (accountReblog || accountBase).get('id') !== me && filtersRegex[1]; const filtered = regex && regex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index')); return statusBase.withMutations(map => { From 68e998de21b8856253858ed800dd0ab6451677b1 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 29 Jun 2019 18:32:06 +0200 Subject: [PATCH 4/6] [Glitch] Use ScrollToOptions for smooth scrolling if supported Port 20b1f8dcf6a4eb63bf78f6e4dd2df67e5eeaded1 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/util/scroll.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/util/scroll.js b/app/javascript/flavours/glitch/util/scroll.js index 2af07e0fb1..84fe582699 100644 --- a/app/javascript/flavours/glitch/util/scroll.js +++ b/app/javascript/flavours/glitch/util/scroll.js @@ -26,5 +26,7 @@ const scroll = (node, key, target) => { }; }; -export const scrollRight = (node, position) => scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => scroll(node, 'scrollTop', 0); +const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; + +export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); +export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); From c49a57e98946e10f71d68baf07527cb32f93f27e Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 30 Jun 2019 00:12:38 +0200 Subject: [PATCH 5/6] [Glitch] Optimize makeGetStatus Port ff65c494a66e9153a5a24386f9b73b58c5ff7692 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/notifications.js | 8 ++--- .../flavours/glitch/selectors/index.js | 35 ++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 2107503db4..c057a52984 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -11,7 +11,7 @@ import { saveSettings } from './settings'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from 'flavours/glitch/util/html'; -import { getFilters, regexFromFilters } from 'flavours/glitch/selectors'; +import { getFiltersRegex } from 'flavours/glitch/selectors'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; @@ -57,13 +57,13 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); - const filters = getFilters(getState(), { contextType: 'notifications' }); + const filters = getFiltersRegex(getState(), { contextType: 'notifications' }); let filtered = false; if (notification.type === 'mention') { - const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible'))); - const regex = regexFromFilters(filters); + const dropRegex = filters[0]; + const regex = filters[1]; const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); if (dropRegex && dropRegex.test(searchIndex)) { diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index 2d4f3f7b4e..9e45825327 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; +import { List as ImmutableList, is } from 'immutable'; import { me } from 'flavours/glitch/util/initial_state'; const getAccountBase = (state, id) => state.getIn(['accounts', id], null); @@ -36,12 +36,10 @@ const toServerSideType = columnType => { } }; -export const getFilters = (state, { contextType }) => state.get('filters', ImmutableList()).filter(filter => contextType && filter.get('context').includes(toServerSideType(contextType)) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); - const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -export const regexFromFilters = filters => { +const regexFromFilters = filters => { if (filters.size === 0) { return null; } @@ -63,6 +61,27 @@ export const regexFromFilters = filters => { }).join('|'), 'i'); }; +// Memoize the filter regexps for each valid server contextType +const makeGetFiltersRegex = () => { + let memo = {}; + + return (state, { contextType }) => { + if (!contextType) return ImmutableList(); + + const serverSideType = toServerSideType(contextType); + const filters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); + + if (!memo[serverSideType] || !is(memo[serverSideType].filters, filters)) { + const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible'))); + const regex = regexFromFilters(filters); + memo[serverSideType] = { filters: filters, results: [dropRegex, regex] }; + } + return memo[serverSideType].results; + }; +}; + +export const getFiltersRegex = makeGetFiltersRegex(); + export const makeGetStatus = () => { return createSelector( [ @@ -70,21 +89,21 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), - getFilters, + getFiltersRegex, ], - (statusBase, statusReblog, accountBase, accountReblog, filters) => { + (statusBase, statusReblog, accountBase, accountReblog, filtersRegex) => { if (!statusBase) { return null; } - const dropRegex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters.filter(filter => filter.get('irreversible'))); + const dropRegex = (accountReblog || accountBase).get('id') !== me && filtersRegex[0]; if (dropRegex && dropRegex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'))) { return null; } - const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters); + const regex = (accountReblog || accountBase).get('id') !== me && filtersRegex[1]; let filtered = false; if (statusReblog) { From 6b21ce2daa2ba28cc2aea92fc728fa812d5f8926 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 30 Jun 2019 11:09:54 +0200 Subject: [PATCH 6/6] [Glitch] When sending a toot, ensure a CW is only set if the CW field is visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partial port of fa7866c9c815b9faa6296492fe8f9dd20650cda4 to glitch-soc It doesn't ensure the field isn't changed, just that it isn't submitted if the field isn't visible. Ensuring the field isn't changed would require reworking the “always show CW field” feature. --- app/javascript/flavours/glitch/actions/compose.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 69cc6827f4..2312bae634 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -138,7 +138,8 @@ export function submitCompose(routerHistory) { return function (dispatch, getState) { let status = getState().getIn(['compose', 'text'], ''); let media = getState().getIn(['compose', 'media_attachments']); - let spoilerText = getState().getIn(['compose', 'spoiler_text'], ''); + const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']); + let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : ''; if ((!status || !status.length) && media.size === 0) { return;