diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 9d496220a3..16de03fd72 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -38,8 +38,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController private def record_login_activity - LoginActivity.create( - user: @user, + @user.login_activities.create( success: true, authentication_method: :omniauth, provider: @provider, diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 2808066aaf..c52bda67b0 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -151,12 +151,11 @@ class Auth::SessionsController < Devise::SessionsController sign_in(user) flash.delete(:notice) - LoginActivity.create( - user: user, - success: true, - authentication_method: security_measure, - ip: request.remote_ip, - user_agent: request.user_agent + user.login_activities.create( + request_details.merge( + authentication_method: security_measure, + success: true + ) ) UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious @@ -167,13 +166,12 @@ class Auth::SessionsController < Devise::SessionsController end def on_authentication_failure(user, security_measure, failure_reason) - LoginActivity.create( - user: user, - success: false, - authentication_method: security_measure, - failure_reason: failure_reason, - ip: request.remote_ip, - user_agent: request.user_agent + user.login_activities.create( + request_details.merge( + authentication_method: security_measure, + failure_reason: failure_reason, + success: false + ) ) # Only send a notification email every hour at most @@ -182,6 +180,13 @@ class Auth::SessionsController < Devise::SessionsController UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! end + def request_details + { + ip: request.remote_ip, + user_agent: request.user_agent, + } + end + def second_factor_attempts_key(user) "2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}" end diff --git a/app/controllers/settings/login_activities_controller.rb b/app/controllers/settings/login_activities_controller.rb index 50e2d70cb9..ae32dbf557 100644 --- a/app/controllers/settings/login_activities_controller.rb +++ b/app/controllers/settings/login_activities_controller.rb @@ -5,6 +5,6 @@ class Settings::LoginActivitiesController < Settings::BaseController skip_before_action :require_functional! def index - @login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page]) + @login_activities = current_user.login_activities.order(id: :desc).page(params[:page]) end end diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 38a0125c1b..9972b507cd 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -31,7 +31,7 @@ module ContextHelper 'quote' => 'https://w3id.org/fep/044f#quote', 'quoteUri' => 'http://fedibird.com/ns#quoteUri', '_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote', - 'quoteAuthorization' => 'https://w3id.org/fep/044f#quoteAuthorization', + 'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' }, }, interaction_policies: { 'gts' => 'https://gotosocial.org/ns#', diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx index 23d77f0dda..d9c87e93a7 100644 --- a/app/javascript/mastodon/components/dropdown_menu.tsx +++ b/app/javascript/mastodon/components/dropdown_menu.tsx @@ -5,6 +5,7 @@ import { useCallback, cloneElement, Children, + useId, } from 'react'; import classNames from 'classnames'; @@ -16,6 +17,7 @@ import Overlay from 'react-overlays/Overlay'; import type { OffsetValue, UsePopperOptions, + Placement, } from 'react-overlays/esm/usePopper'; import { fetchRelationships } from 'mastodon/actions/accounts'; @@ -295,6 +297,11 @@ interface DropdownProps { title?: string; disabled?: boolean; scrollable?: boolean; + placement?: Placement; + /** + * Prevent the `ScrollableList` with this scrollKey + * from being scrolled while the dropdown is open + */ scrollKey?: string; status?: ImmutableMap; forceDropdown?: boolean; @@ -316,6 +323,7 @@ export const Dropdown = ({ title = 'Menu', disabled, scrollable, + placement = 'bottom', status, forceDropdown = false, renderItem, @@ -331,16 +339,15 @@ export const Dropdown = ({ ); const [currentId] = useState(id++); const open = currentId === openDropdownId; - const activeElement = useRef(null); - const targetRef = useRef(null); + const buttonRef = useRef(null); + const menuId = useId(); const prefetchAccountId = status ? status.getIn(['account', 'id']) : undefined; const handleClose = useCallback(() => { - if (activeElement.current) { - activeElement.current.focus({ preventScroll: true }); - activeElement.current = null; + if (buttonRef.current) { + buttonRef.current.focus({ preventScroll: true }); } dispatch( @@ -375,7 +382,7 @@ export const Dropdown = ({ [handleClose, onItemClick, items], ); - const handleClick = useCallback( + const toggleDropdown = useCallback( (e: React.MouseEvent | React.KeyboardEvent) => { const { type } = e; @@ -423,38 +430,6 @@ export const Dropdown = ({ ], ); - const handleMouseDown = useCallback(() => { - if (!open && document.activeElement instanceof HTMLElement) { - activeElement.current = document.activeElement; - } - }, [open]); - - const handleButtonKeyDown = useCallback( - (e: React.KeyboardEvent) => { - switch (e.key) { - case ' ': - case 'Enter': - handleMouseDown(); - break; - } - }, - [handleMouseDown], - ); - - const handleKeyPress = useCallback( - (e: React.KeyboardEvent) => { - switch (e.key) { - case ' ': - case 'Enter': - handleClick(e); - e.stopPropagation(); - e.preventDefault(); - break; - } - }, - [handleClick], - ); - useEffect(() => { return () => { if (currentId === openDropdownId) { @@ -465,14 +440,16 @@ export const Dropdown = ({ let button: React.ReactElement; + const buttonProps = { + disabled, + onClick: toggleDropdown, + 'aria-expanded': open, + 'aria-controls': menuId, + ref: buttonRef, + }; + if (children) { - button = cloneElement(Children.only(children), { - onClick: handleClick, - onMouseDown: handleMouseDown, - onKeyDown: handleButtonKeyDown, - onKeyPress: handleKeyPress, - ref: targetRef, - }); + button = cloneElement(Children.only(children), buttonProps); } else if (icon && iconComponent) { button = ( ({ iconComponent={iconComponent} title={title} active={open} - disabled={disabled} - onClick={handleClick} - onMouseDown={handleMouseDown} - onKeyDown={handleButtonKeyDown} - onKeyPress={handleKeyPress} - ref={targetRef} + {...buttonProps} /> ); } else { @@ -499,13 +471,13 @@ export const Dropdown = ({ {({ props, arrowProps, placement }) => ( -
+
; onMouseDown?: React.MouseEventHandler; onKeyDown?: React.KeyboardEventHandler; - onKeyPress?: React.KeyboardEventHandler; active?: boolean; expanded?: boolean; style?: React.CSSProperties; @@ -45,7 +44,6 @@ export const IconButton = forwardRef( activeStyle, onClick, onKeyDown, - onKeyPress, onMouseDown, active = false, disabled = false, @@ -85,16 +83,6 @@ export const IconButton = forwardRef( [disabled, onClick], ); - const handleKeyPress: React.KeyboardEventHandler = - useCallback( - (e) => { - if (!disabled) { - onKeyPress?.(e); - } - }, - [disabled, onKeyPress], - ); - const handleMouseDown: React.MouseEventHandler = useCallback( (e) => { @@ -161,7 +149,6 @@ export const IconButton = forwardRef( onClick={handleClick} onMouseDown={handleMouseDown} onKeyDown={handleKeyDown} - onKeyPress={handleKeyPress} // eslint-disable-line @typescript-eslint/no-deprecated style={buttonStyle} tabIndex={tabIndex} disabled={disabled} diff --git a/app/javascript/mastodon/features/navigation_panel/components/more_link.tsx b/app/javascript/mastodon/features/navigation_panel/components/more_link.tsx index a26935eacf..a3477ec4e5 100644 --- a/app/javascript/mastodon/features/navigation_panel/components/more_link.tsx +++ b/app/javascript/mastodon/features/navigation_panel/components/more_link.tsx @@ -50,16 +50,22 @@ export const MoreLink: React.FC = () => { const menu = useMemo(() => { const arr: MenuItem[] = [ - { text: intl.formatMessage(messages.filters), href: '/filters' }, - { text: intl.formatMessage(messages.mutes), to: '/mutes' }, - { text: intl.formatMessage(messages.blocks), to: '/blocks' }, { - text: intl.formatMessage(messages.domainBlocks), - to: '/domain_blocks', + href: '/filters', + text: intl.formatMessage(messages.filters), + }, + { + to: '/mutes', + text: intl.formatMessage(messages.mutes), + }, + { + to: '/blocks', + text: intl.formatMessage(messages.blocks), + }, + { + to: '/domain_blocks', + text: intl.formatMessage(messages.domainBlocks), }, - ]; - - arr.push( null, { href: '/settings/privacy', @@ -77,7 +83,7 @@ export const MoreLink: React.FC = () => { href: '/settings/export', text: intl.formatMessage(messages.importExport), }, - ); + ]; if (canManageReports(permissions)) { arr.push(null, { @@ -106,7 +112,7 @@ export const MoreLink: React.FC = () => { }, [intl, dispatch, permissions]); return ( - +