diff --git a/Gemfile.lock b/Gemfile.lock index b4d58cbb69..db436c01be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -345,7 +345,7 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.13.2) + json (2.15.0) json-canonicalization (1.0.0) json-jwt (1.16.7) activesupport (>= 4.2) @@ -626,7 +626,7 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) prettyprint (0.2.0) - prism (1.4.0) + prism (1.5.1) prometheus_exporter (2.3.0) webrick propshaft (1.3.1) @@ -722,7 +722,7 @@ GEM redis (4.8.1) redis-client (0.26.0) connection_pool - regexp_parser (2.11.2) + regexp_parser (2.11.3) reline (0.6.2) io-console (~> 0.5) request_store (1.7.0) @@ -766,7 +766,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.4) - rubocop (1.80.2) + rubocop (1.81.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -774,10 +774,10 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.46.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.46.0) + rubocop-ast (1.47.1) parser (>= 3.3.7.2) prism (~> 1.4) rubocop-capybara (2.22.1) @@ -901,9 +901,9 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.9.1) - unicode-display_width (3.1.5) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) uri (1.0.3) useragent (0.16.11) validate_url (1.0.15) diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx index 815b4b59ab..1dc1d45083 100644 --- a/app/javascript/mastodon/components/router.tsx +++ b/app/javascript/mastodon/components/router.tsx @@ -1,6 +1,7 @@ import type { PropsWithChildren } from 'react'; import type React from 'react'; +import type { useLocation } from 'react-router'; import { Router as OriginalRouter, useHistory } from 'react-router'; import type { @@ -18,7 +19,9 @@ interface MastodonLocationState { mastodonModalKey?: string; } -type LocationState = MastodonLocationState | null | undefined; +export type LocationState = MastodonLocationState | null | undefined; + +export type MastodonLocation = ReturnType>; type HistoryPath = Path | LocationDescriptor; diff --git a/app/javascript/mastodon/components/scrollable_list.jsx b/app/javascript/mastodon/components/scrollable_list.jsx index 22ec18afa9..38c3cd991b 100644 --- a/app/javascript/mastodon/components/scrollable_list.jsx +++ b/app/javascript/mastodon/components/scrollable_list.jsx @@ -10,7 +10,7 @@ import { connect } from 'react-redux'; import { supportsPassiveEvents } from 'detect-passive-events'; import { throttle } from 'lodash'; -import ScrollContainer from 'mastodon/containers/scroll_container'; +import { ScrollContainer } from 'mastodon/containers/scroll_container'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; @@ -399,7 +399,7 @@ class ScrollableList extends PureComponent { if (trackScroll) { return ( - + {scrollableArea} ); diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 8dcda3b0a9..086a7681c4 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -5,7 +5,6 @@ import { Route } from 'react-router-dom'; import { Provider as ReduxProvider } from 'react-redux'; -import { ScrollContext } from 'react-router-scroll-4'; import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis'; import { hydrateStore } from 'mastodon/actions/store'; @@ -20,6 +19,8 @@ import { store } from 'mastodon/store'; import { isProduction } from 'mastodon/utils/environment'; import { BodyScrollLock } from 'mastodon/features/ui/components/body_scroll_lock'; +import { ScrollContext } from './scroll_container/scroll_context'; + const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`; const hydrateAction = hydrateStore(initialState); @@ -45,10 +46,6 @@ export default class Mastodon extends PureComponent { } } - shouldUpdateScroll (prevRouterProps, { location }) { - return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey); - } - render () { return ( @@ -56,7 +53,7 @@ export default class Mastodon extends PureComponent { - + diff --git a/app/javascript/mastodon/containers/scroll_container.js b/app/javascript/mastodon/containers/scroll_container.js deleted file mode 100644 index d21ff63687..0000000000 --- a/app/javascript/mastodon/containers/scroll_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4'; - -// ScrollContainer is used to automatically scroll to the top when pushing a -// new history state and remembering the scroll position when going back. -// There are a few things we need to do differently, though. -const defaultShouldUpdateScroll = (prevRouterProps, { location }) => { - // If the change is caused by opening a modal, do not scroll to top - return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey); -}; - -export default -class ScrollContainer extends OriginalScrollContainer { - - static defaultProps = { - shouldUpdateScroll: defaultShouldUpdateScroll, - }; - -} diff --git a/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx b/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx new file mode 100644 index 0000000000..b8726a1a75 --- /dev/null +++ b/app/javascript/mastodon/containers/scroll_container/default_should_update_scroll.tsx @@ -0,0 +1,25 @@ +import type { MastodonLocation } from 'mastodon/components/router'; + +export type ShouldUpdateScrollFn = ( + prevLocationContext: MastodonLocation | null, + locationContext: MastodonLocation, +) => boolean; + +/** + * ScrollBehavior will automatically scroll to the top on navigations + * or restore saved scroll positions, but on some location changes we + * need to prevent this. + */ + +export const defaultShouldUpdateScroll: ShouldUpdateScrollFn = ( + prevLocation, + location, +) => { + // If the change is caused by opening a modal, do not scroll to top + const shouldUpdateScroll = !( + location.state?.mastodonModalKey && + location.state.mastodonModalKey !== prevLocation?.state?.mastodonModalKey + ); + + return shouldUpdateScroll; +}; diff --git a/app/javascript/mastodon/containers/scroll_container/index.tsx b/app/javascript/mastodon/containers/scroll_container/index.tsx new file mode 100644 index 0000000000..0d0ab364dc --- /dev/null +++ b/app/javascript/mastodon/containers/scroll_container/index.tsx @@ -0,0 +1,76 @@ +import React, { + useContext, + useEffect, + useImperativeHandle, + useRef, +} from 'react'; + +import { defaultShouldUpdateScroll } from './default_should_update_scroll'; +import type { ShouldUpdateScrollFn } from './default_should_update_scroll'; +import { ScrollBehaviorContext } from './scroll_context'; + +interface ScrollContainerProps { + /** + * This key must be static for the element & not change + * while the component is mounted. + */ + scrollKey: string; + shouldUpdateScroll?: ShouldUpdateScrollFn; + childRef?: React.ForwardedRef; + children: React.ReactElement; +} + +/** + * `ScrollContainer` is used to manage the scroll position of elements on the page + * that can be scrolled independently of the page body. + * This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/ + */ + +export const ScrollContainer: React.FC = ({ + children, + scrollKey, + childRef, + shouldUpdateScroll = defaultShouldUpdateScroll, +}) => { + const scrollBehaviorContext = useContext(ScrollBehaviorContext); + + const containerRef = useRef(); + + /** + * If a childRef is passed, sync it with the containerRef. This + * is necessary because in this component's return statement, + * we're overwriting the immediate child component's ref prop. + */ + useImperativeHandle(childRef, () => containerRef.current, []); + + /** + * Register/unregister scrollable element with ScrollBehavior + */ + useEffect(() => { + if (!scrollBehaviorContext || !containerRef.current) { + return; + } + + scrollBehaviorContext.registerElement( + scrollKey, + containerRef.current, + (prevLocation, location) => { + // Hack to allow accessing scrollBehavior._stateStorage + return shouldUpdateScroll.call( + scrollBehaviorContext.scrollBehavior, + prevLocation, + location, + ); + }, + ); + + return () => { + scrollBehaviorContext.unregisterElement(scrollKey); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return React.Children.only( + React.cloneElement(children, { ref: containerRef }), + ); +}; diff --git a/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx b/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx new file mode 100644 index 0000000000..a7eb780800 --- /dev/null +++ b/app/javascript/mastodon/containers/scroll_container/scroll_context.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; + +import { useLocation, useHistory } from 'react-router-dom'; + +import type { LocationBase } from 'scroll-behavior'; +import ScrollBehavior from 'scroll-behavior'; + +import type { + LocationState, + MastodonLocation, +} from 'mastodon/components/router'; +import { usePrevious } from 'mastodon/hooks/usePrevious'; + +import { defaultShouldUpdateScroll } from './default_should_update_scroll'; +import type { ShouldUpdateScrollFn } from './default_should_update_scroll'; +import { SessionStorage } from './state_storage'; + +type ScrollBehaviorInstance = InstanceType< + typeof ScrollBehavior +>; + +export interface ScrollBehaviorContextType { + registerElement: ( + key: string, + element: HTMLElement, + shouldUpdateScroll: ( + prevLocationContext: MastodonLocation | null, + locationContext: MastodonLocation, + ) => boolean, + ) => void; + unregisterElement: (key: string) => void; + scrollBehavior?: ScrollBehaviorInstance; +} + +export const ScrollBehaviorContext = + React.createContext(null); + +interface ScrollContextProps { + shouldUpdateScroll?: ShouldUpdateScrollFn; + children: React.ReactElement; +} + +/** + * A top-level wrapper that provides the app with an instance of the + * ScrollBehavior object. scroll-behavior is a library for managing the + * scroll position of a single-page app in the same way the browser would + * normally do for a multi-page app. This means it'll scroll back to top + * when navigating to a new page, and will restore the scroll position + * when navigating e.g. using `history.back`. + * The library keeps a record of scroll positions in session storage. + * + * This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/ + */ + +export const ScrollContext: React.FC = ({ + children, + shouldUpdateScroll = defaultShouldUpdateScroll, +}) => { + const location = useLocation(); + const history = useHistory(); + + /** + * Keep the current location in a mutable ref so that ScrollBehavior's + * `getCurrentLocation` can access it without having to recreate the + * whole ScrollBehavior object + */ + const currentLocationRef = useRef(location); + useEffect(() => { + currentLocationRef.current = location; + }, [location]); + + /** + * Initialise ScrollBehavior object once – using state rather + * than a ref to simplify the types and ensure it's defined immediately. + */ + const [scrollBehavior] = useState( + (): ScrollBehaviorInstance => + new ScrollBehavior({ + addNavigationListener: history.listen.bind(history), + stateStorage: new SessionStorage(), + getCurrentLocation: () => + currentLocationRef.current as unknown as LocationBase, + shouldUpdateScroll: ( + prevLocationContext: MastodonLocation | null, + locationContext: MastodonLocation, + ) => + // Hack to allow accessing scrollBehavior._stateStorage + shouldUpdateScroll.call( + scrollBehavior, + prevLocationContext, + locationContext, + ), + }), + ); + + /** + * Handle scroll update when location changes + */ + const prevLocation = usePrevious(location) ?? null; + useEffect(() => { + scrollBehavior.updateScroll(prevLocation, location); + }, [location, prevLocation, scrollBehavior]); + + /** + * Stop Scrollbehavior on unmount + */ + useEffect(() => { + return () => { + scrollBehavior.stop(); + }; + }, [scrollBehavior]); + + /** + * Provide the app with a way to register separately scrollable + * elements to also be tracked by ScrollBehavior. (By default + * ScrollBehavior only handles scrolling on the main document body.) + */ + const contextValue = useMemo( + () => ({ + registerElement: (key, element, shouldUpdateScroll) => { + scrollBehavior.registerElement( + key, + element, + shouldUpdateScroll, + location, + ); + }, + unregisterElement: (key) => { + scrollBehavior.unregisterElement(key); + }, + scrollBehavior, + }), + [location, scrollBehavior], + ); + + return ( + + {React.Children.only(children)} + + ); +}; diff --git a/app/javascript/mastodon/containers/scroll_container/state_storage.ts b/app/javascript/mastodon/containers/scroll_container/state_storage.ts new file mode 100644 index 0000000000..fe8a208aae --- /dev/null +++ b/app/javascript/mastodon/containers/scroll_container/state_storage.ts @@ -0,0 +1,46 @@ +import type { LocationBase, ScrollPosition } from 'scroll-behavior'; + +const STATE_KEY_PREFIX = '@@scroll|'; + +interface LocationBaseWithKey extends LocationBase { + key?: string; +} + +/** + * This module is part of our port of https://github.com/ytase/react-router-scroll/ + * and handles storing scroll positions in SessionStorage. + * Stored positions (`[x, y]`) are keyed by the location key and an optional + * `scrollKey` that's used for to track separately scrollable elements other + * than the document body. + */ + +export class SessionStorage { + read( + location: LocationBaseWithKey, + key: string | null, + ): ScrollPosition | null { + const stateKey = this.getStateKey(location, key); + + try { + const value = sessionStorage.getItem(stateKey); + return value ? (JSON.parse(value) as ScrollPosition) : null; + } catch { + return null; + } + } + + save(location: LocationBaseWithKey, key: string | null, value: unknown) { + const stateKey = this.getStateKey(location, key); + const storedValue = JSON.stringify(value); + + try { + sessionStorage.setItem(stateKey, storedValue); + } catch {} + } + + getStateKey(location: LocationBaseWithKey, key: string | null) { + const locationKey = location.key; + const stateKeyBase = `${STATE_KEY_PREFIX}${locationKey}`; + return key == null ? stateKeyBase : `${stateKeyBase}|${key}`; + } +} diff --git a/app/javascript/mastodon/features/directory/index.tsx b/app/javascript/mastodon/features/directory/index.tsx index a29febcd1a..0fe140b4eb 100644 --- a/app/javascript/mastodon/features/directory/index.tsx +++ b/app/javascript/mastodon/features/directory/index.tsx @@ -21,7 +21,7 @@ import { ColumnHeader } from 'mastodon/components/column_header'; import { LoadMore } from 'mastodon/components/load_more'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { RadioButton } from 'mastodon/components/radio_button'; -import ScrollContainer from 'mastodon/containers/scroll_container'; +import { ScrollContainer } from 'mastodon/containers/scroll_container'; import { useSearchParam } from 'mastodon/hooks/useSearchParam'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; @@ -206,7 +206,6 @@ export const Directory: React.FC<{ /> {multiColumn && !pinned ? ( - // @ts-expect-error ScrollContainer is not properly typed yet {scrollableArea} diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 404faf609e..7c38af3277 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -16,7 +16,7 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac import { Hotkeys } from 'mastodon/components/hotkeys'; import { Icon } from 'mastodon/components/icon'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; -import ScrollContainer from 'mastodon/containers/scroll_container'; +import { ScrollContainer } from 'mastodon/containers/scroll_container'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -526,9 +526,9 @@ class Status extends ImmutablePureComponent { this.setState({ fullscreen: isFullscreen() }); }; - shouldUpdateScroll = (prevRouterProps, { location }) => { + shouldUpdateScroll = (prevLocation, location) => { // Do not change scroll when opening a modal - if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) { + if (location.state?.mastodonModalKey !== prevLocation?.state?.mastodonModalKey) { return false; } @@ -602,7 +602,7 @@ class Status extends ImmutablePureComponent { )} /> - +
{ancestors} diff --git a/app/javascript/mastodon/features/ui/util/focusUtils.ts b/app/javascript/mastodon/features/ui/util/focusUtils.ts index a19852e0d2..9bcd3f8943 100644 --- a/app/javascript/mastodon/features/ui/util/focusUtils.ts +++ b/app/javascript/mastodon/features/ui/util/focusUtils.ts @@ -60,23 +60,13 @@ export function focusColumn({ * Get the index of the currently focused item in one of our item lists */ export function getFocusedItemIndex() { - const focusedElement = document.activeElement; - const itemList = focusedElement?.closest('.item-list'); - - if (!focusedElement || !itemList) { - return -1; - } - - let focusedItem: HTMLElement | null = null; - if (focusedElement.parentElement === itemList) { - focusedItem = focusedElement as HTMLElement; - } else { - focusedItem = focusedElement.closest('.item-list > *'); - } - + const focusedItem = document.activeElement?.closest('.item-list > *'); if (!focusedItem) return -1; - const items = Array.from(itemList.children); + const { parentElement } = focusedItem; + if (!parentElement) return -1; + + const items = Array.from(parentElement.children); return items.indexOf(focusedItem); } diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index c61938faf5..6086fbf77f 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -864,6 +864,14 @@ "status.cancel_reblog_private": "Прыбраць", "status.cannot_quote": "Вы не маеце дазвол цытаваць гэты допіс", "status.cannot_reblog": "Гэты допіс нельга пашырыць", + "status.contains_quote": "Утрымлівае цытату", + "status.context.loading": "Загружаюцца іншыя адказы", + "status.context.loading_error": "Немагчыма загрузіць новыя адказы", + "status.context.loading_more": "Загружаюцца іншыя адказы", + "status.context.loading_success": "Усе адказы загружаныя", + "status.context.more_replies_found": "Знойдзеныя іншыя адказы", + "status.context.retry": "Паспрабаваць зноў", + "status.context.show": "Паказаць", "status.continued_thread": "Працяг ланцужка", "status.copy": "Скапіраваць спасылку на допіс", "status.delete": "Выдаліць", @@ -901,6 +909,7 @@ "status.quote_error.revoked": "Аўтар выдаліў допіс", "status.quote_followers_only": "Толькі падпісчыкі могуць цытаваць гэты допіс", "status.quote_manual_review": "Аўтар зробіць агляд уручную", + "status.quote_noun": "Цытаваць", "status.quote_policy_change": "Змяніць, хто можа цытаваць", "status.quote_post_author": "Цытаваў допіс @{name}", "status.quote_private": "Прыватныя допісы нельга цытаваць", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 3b4138cbc8..d972d4705b 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Nemáte oprávnění citovat tento příspěvek", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", "status.contains_quote": "Obsahuje citaci", + "status.context.loading": "Načítání dalších odpovědí", + "status.context.loading_error": "Nelze načíst nové odpovědi", + "status.context.loading_more": "Načítání dalších odpovědí", + "status.context.loading_success": "Všechny odpovědi načteny", + "status.context.more_replies_found": "Nalezeny další odpovědi", + "status.context.retry": "Zkusit znovu", + "status.context.show": "Zobrazit", "status.continued_thread": "Pokračuje ve vlákně", "status.copy": "Zkopírovat odkaz na příspěvek", "status.delete": "Smazat", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 910ed0c19a..e756b6bab6 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -865,12 +865,12 @@ "status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden", "status.contains_quote": "Enthält Zitat", - "status.context.loading": "Weitere Antworten werden geladen", - "status.context.loading_error": "Neue Antworten konnten nicht geladen werden", - "status.context.loading_more": "Weitere Antworten werden geladen", - "status.context.loading_success": "Alle Antworten geladen", + "status.context.loading": "Weitere Antworten laden", + "status.context.loading_error": "Weitere Antworten konnten nicht geladen werden", + "status.context.loading_more": "Weitere Antworten laden", + "status.context.loading_success": "Alle weiteren Antworten geladen", "status.context.more_replies_found": "Weitere Antworten verfügbar", - "status.context.retry": "Wiederholen", + "status.context.retry": "Erneut versuchen", "status.context.show": "Anzeigen", "status.continued_thread": "Fortgeführter Thread", "status.copy": "Link zum Beitrag kopieren", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index b57383da23..b6e02a0ba6 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Sul pole õigust seda postitust tsiteerida", "status.cannot_reblog": "Seda postitust ei saa jagada", "status.contains_quote": "Sisaldab tsitaati", + "status.context.loading": "Laadin veel vastuseid", + "status.context.loading_error": "Uute vastuste laadimine ei õnnestunud", + "status.context.loading_more": "Laadin veel vastuseid", + "status.context.loading_success": "Kõik vastused on laaditud", + "status.context.more_replies_found": "Leidub veel vastuseid", + "status.context.retry": "Proovi uuesti", + "status.context.show": "Näita", "status.continued_thread": "Jätkatud lõim", "status.copy": "Kopeeri postituse link", "status.delete": "Kustuta", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 348de34eb1..938db30d7a 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Tú hevur ikki loyvi at sitera hendan postin", "status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin", "status.contains_quote": "Inniheldur sitat", + "status.context.loading": "Tekur fleiri svar niður", + "status.context.loading_error": "Fekk ikki tikið nýggj svar niður", + "status.context.loading_more": "Tekur fleiri svar niður", + "status.context.loading_success": "Øll svar tikin niður", + "status.context.more_replies_found": "Fleiri svar funnin", + "status.context.retry": "Royn aftur", + "status.context.show": "Vís", "status.continued_thread": "Framhaldandi tráður", "status.copy": "Kopiera leinki til postin", "status.delete": "Strika", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 2277128032..88d03ea43c 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Ní cheadaítear duit an post seo a lua", "status.cannot_reblog": "Ní féidir an phostáil seo a mholadh", "status.contains_quote": "Tá luachan ann", + "status.context.loading": "Ag lódáil tuilleadh freagraí", + "status.context.loading_error": "Níorbh fhéidir freagraí nua a lódáil", + "status.context.loading_more": "Ag lódáil tuilleadh freagraí", + "status.context.loading_success": "Luchtaithe na freagraí uile", + "status.context.more_replies_found": "Tuilleadh freagraí aimsithe", + "status.context.retry": "Déan iarracht arís", + "status.context.show": "Taispeáin", "status.continued_thread": "Snáithe ar lean", "status.copy": "Cóipeáil an nasc chuig an bpostáil", "status.delete": "Scrios", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 894a93b5fb..4250040040 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -865,6 +865,13 @@ "status.cannot_quote": "Je bent niet gemachtigd om dit bericht te citeren", "status.cannot_reblog": "Dit bericht kan niet geboost worden", "status.contains_quote": "Bevat citaat", + "status.context.loading": "Meer reacties laden", + "status.context.loading_error": "Kon geen nieuwe reacties laden", + "status.context.loading_more": "Meer reacties laden", + "status.context.loading_success": "Alle reacties zijn geladen", + "status.context.more_replies_found": "Meer reacties gevonden", + "status.context.retry": "Opnieuw proberen", + "status.context.show": "Tonen", "status.continued_thread": "Vervolg van gesprek", "status.copy": "Link naar bericht kopiëren", "status.delete": "Verwijderen", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index b390a8a8e5..614b268ec7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2972,8 +2972,12 @@ a.account__display-name { justify-content: flex-start; position: relative; - &.unscrollable { - overflow-x: hidden; + .layout-multiple-columns & { + overflow-x: auto; + + &.unscrollable { + overflow-x: hidden; + } } &__panels { diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 6314fe1fd5..7647adb7e6 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -97,15 +97,17 @@ class Form::AdminSettings }.freeze DESCRIPTION_LIMIT = 200 + DOMAIN_BLOCK_AUDIENCES = %w(disabled users all).freeze + REGISTRATION_MODES = %w(open approved none).freeze attr_accessor(*KEYS) - validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) } + validates :registrations_mode, inclusion: { in: REGISTRATION_MODES }, if: -> { defined?(@registrations_mode) } validates :site_contact_email, :site_contact_username, presence: true, if: -> { defined?(@site_contact_username) || defined?(@site_contact_email) } validates :site_contact_username, existing_username: true, if: -> { defined?(@site_contact_username) } validates :bootstrap_timeline_accounts, existing_username: { multiple: true }, if: -> { defined?(@bootstrap_timeline_accounts) } - validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) } - validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } + validates :show_domain_blocks, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks) } + validates :show_domain_blocks_rationale, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks_rationale) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) } validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) } diff --git a/app/views/admin/settings/about/show.html.haml b/app/views/admin/settings/about/show.html.haml index 1eb47a0b54..adc8f1ff04 100644 --- a/app/views/admin/settings/about/show.html.haml +++ b/app/views/admin/settings/about/show.html.haml @@ -24,7 +24,7 @@ .fields-row__column.fields-row__column-6.fields-group = f.input :show_domain_blocks, collection_wrapper_tag: 'ul', - collection: %i(disabled users all), + collection: f.object.class::DOMAIN_BLOCK_AUDIENCES, include_blank: false, item_wrapper_tag: 'li', label_method: ->(value) { t("admin.settings.domain_blocks.#{value}") }, @@ -32,7 +32,7 @@ .fields-row__column.fields-row__column-6.fields-group = f.input :show_domain_blocks_rationale, collection_wrapper_tag: 'ul', - collection: %i(disabled users all), + collection: f.object.class::DOMAIN_BLOCK_AUDIENCES, include_blank: false, item_wrapper_tag: 'li', label_method: ->(value) { t("admin.settings.domain_blocks.#{value}") }, diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index cb5a3eb6ba..7303eca662 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -18,7 +18,7 @@ .fields-row .fields-row__column.fields-row__column-6.fields-group = f.input :registrations_mode, - collection: %w(open approved none), + collection: f.object.class::REGISTRATION_MODES, include_blank: false, label_method: ->(mode) { I18n.t("admin.settings.registrations_mode.modes.#{mode}") }, warning_hint: I18n.t('admin.settings.registrations_mode.warning_hint'), diff --git a/package.json b/package.json index 3158fd5bae..1105185a98 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,6 @@ "react-redux-loading-bar": "^5.0.8", "react-router": "^5.3.4", "react-router-dom": "^5.3.4", - "react-router-scroll-4": "^1.0.0-beta.1", "react-select": "^5.7.3", "react-sparklines": "^1.7.0", "react-swipeable-views": "^0.14.0", @@ -113,6 +112,7 @@ "rollup-plugin-gzip": "^4.1.1", "rollup-plugin-visualizer": "^6.0.3", "sass": "^1.62.1", + "scroll-behavior": "^0.11.0", "stacktrace-js": "^2.0.2", "stringz": "^2.1.0", "substring-trie": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index 5e63eea9e4..4ef1097d40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2838,7 +2838,6 @@ __metadata: react-redux-loading-bar: "npm:^5.0.8" react-router: "npm:^5.3.4" react-router-dom: "npm:^5.3.4" - react-router-scroll-4: "npm:^1.0.0-beta.1" react-select: "npm:^5.7.3" react-sparklines: "npm:^1.7.0" react-swipeable-views: "npm:^0.14.0" @@ -2851,6 +2850,7 @@ __metadata: rollup-plugin-gzip: "npm:^4.1.1" rollup-plugin-visualizer: "npm:^6.0.3" sass: "npm:^1.62.1" + scroll-behavior: "npm:^0.11.0" stacktrace-js: "npm:^2.0.2" storybook: "npm:^9.1.1" stringz: "npm:^2.1.0" @@ -6487,16 +6487,7 @@ __metadata: languageName: node linkType: hard -"dom-helpers@npm:^3.4.0": - version: 3.4.0 - resolution: "dom-helpers@npm:3.4.0" - dependencies: - "@babel/runtime": "npm:^7.1.2" - checksum: 10c0/1d2d3e4eadac2c4f4c8c7470a737ab32b7ec28237c4d094ea967ec3184168fd12452196fcc424a5d7860b6176117301aeaecba39467bf1a6e8492a8e5c9639d1 - languageName: node - linkType: hard - -"dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.2.0": +"dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.1.4, dom-helpers@npm:^5.2.0": version: 5.2.1 resolution: "dom-helpers@npm:5.2.1" dependencies: @@ -10052,6 +10043,13 @@ __metadata: languageName: node linkType: hard +"page-lifecycle@npm:^0.1.2": + version: 0.1.2 + resolution: "page-lifecycle@npm:0.1.2" + checksum: 10c0/509dbbc2ad2000dffcf591f66ab13d80fb1dba9337d85c76269173f7a5c3959b5a876e3bfb1e4494f6b932c1dc02a0b5824ebd452ab1a7204d4abdf498cb27c5 + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -11293,21 +11291,6 @@ __metadata: languageName: node linkType: hard -"react-router-scroll-4@npm:^1.0.0-beta.1": - version: 1.0.0-beta.2 - resolution: "react-router-scroll-4@npm:1.0.0-beta.2" - dependencies: - scroll-behavior: "npm:^0.9.1" - warning: "npm:^3.0.0" - peerDependencies: - prop-types: ^15.6.0 - react: ^15.0.0 || ^16.0.0 - react-dom: ^15.0.0 || ^16.0.0 - react-router-dom: ^4.0 - checksum: 10c0/ad195b7359fd3146530cf299ec437f0a619c577b2cacfb2c76a156d3cd9d5d3e97af56e17c300c37ca8c485041e93124fe63f0c86db6aea468caf838281e62cb - languageName: node - linkType: hard - "react-router@npm:5.3.4, react-router@npm:^5.3.4": version: 5.3.4 resolution: "react-router@npm:5.3.4" @@ -12067,13 +12050,14 @@ __metadata: languageName: node linkType: hard -"scroll-behavior@npm:^0.9.1": - version: 0.9.12 - resolution: "scroll-behavior@npm:0.9.12" +"scroll-behavior@npm:^0.11.0": + version: 0.11.0 + resolution: "scroll-behavior@npm:0.11.0" dependencies: - dom-helpers: "npm:^3.4.0" + dom-helpers: "npm:^5.1.4" invariant: "npm:^2.2.4" - checksum: 10c0/4f438c48b93a1dcc2ab51a18670fac6f5ce41885291d8aa13251b4a187be9d0c6dd518ee974eb52ac9bbe227b9811c2615ecca73192a1a190b78dfdadb9c2cf2 + page-lifecycle: "npm:^0.1.2" + checksum: 10c0/c54010c9fdd9fc360fd7887ecf64f16972f9557ac679723709612cd54fc4778c7433ab46a9637933179ef31471f78e2591fb35351dc0e15537fecf1c8c89d32c languageName: node linkType: hard @@ -14029,15 +14013,6 @@ __metadata: languageName: node linkType: hard -"warning@npm:^3.0.0": - version: 3.0.0 - resolution: "warning@npm:3.0.0" - dependencies: - loose-envify: "npm:^1.0.0" - checksum: 10c0/6a2a56ab3139d3927193d926a027e74e1449fa47cc692feea95f8a81a4bb5b7f10c312def94cce03f3b58cb26ba3247858e75d17d596451d2c483a62e8204705 - languageName: node - linkType: hard - "warning@npm:^4.0.1, warning@npm:^4.0.3": version: 4.0.3 resolution: "warning@npm:4.0.3"