diff options
author | Claire <claire.github-309c@sitedethib.com> | 2023-04-22 10:06:11 +0200 |
---|---|---|
committer | Claire <claire.github-309c@sitedethib.com> | 2023-04-22 10:06:11 +0200 |
commit | abfdafef1ededdb87f018414edd6b25fa9a70525 (patch) | |
tree | 7a9855d79d125333a6b1307215b73dd507475320 /app | |
parent | f30c5e7f15f967019245d2c78f3c2e89800eb838 (diff) | |
parent | 4db8230194258a9a1c3d17d7261608515f3f2067 (diff) |
Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/controllers/auth/setup_controller.rb`: Upstream removed a method close to a glitch-soc theming-related method. Removed the method like upstream did.
Diffstat (limited to 'app')
37 files changed, 583 insertions, 276 deletions
diff --git a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb new file mode 100644 index 000000000..5d9fcc82c --- /dev/null +++ b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseController + include Authorization + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + before_action :set_providers, only: :index + + after_action :verify_authorized + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i(limit).freeze + + def index + authorize :preview_card_provider, :index? + + render json: @providers, each_serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + def approve + authorize :preview_card_provider, :review? + + provider = PreviewCardProvider.find(params[:id]) + provider.update(trendable: true, reviewed_at: Time.now.utc) + render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + def reject + authorize :preview_card_provider, :review? + + provider = PreviewCardProvider.find(params[:id]) + provider.update(trendable: false, reviewed_at: Time.now.utc) + render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + private + + def set_providers + @providers = PreviewCardProvider.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_trends_links_preview_card_providers_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty? + end + + def pagination_max_id + @providers.last.id + end + + def pagination_since_id + @providers.first.id + end + + def records_continue? + @providers.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb index cc6388980..7f4ca4828 100644 --- a/app/controllers/api/v1/admin/trends/links_controller.rb +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::LinksController < Api::V1::Trends::LinksController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index + + def index + if current_user&.can?(:manage_taxonomies) + render json: @links, each_serializer: REST::Admin::Trends::LinkSerializer + else + super + end + end + + def approve + authorize :preview_card, :review? + + link = PreviewCard.find(params[:id]) + link.update(trendable: true) + render json: link, serializer: REST::Admin::Trends::LinkSerializer + end + + def reject + authorize :preview_card, :review? + + link = PreviewCard.find(params[:id]) + link.update(trendable: false) + render json: link, serializer: REST::Admin::Trends::LinkSerializer + end private diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb index c39f77363..34b6580df 100644 --- a/app/controllers/api/v1/admin/trends/statuses_controller.rb +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::StatusesController < Api::V1::Trends::StatusesController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index + + def index + if current_user&.can?(:manage_taxonomies) + render json: @statuses, each_serializer: REST::Admin::Trends::StatusSerializer + else + super + end + end + + def approve + authorize [:admin, :status], :review? + + status = Status.find(params[:id]) + status.update(trendable: true) + render json: status, serializer: REST::Admin::Trends::StatusSerializer + end + + def reject + authorize [:admin, :status], :review? + + status = Status.find(params[:id]) + status.update(trendable: false) + render json: status, serializer: REST::Admin::Trends::StatusSerializer + end private diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index e77df3021..2eeea9522 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index def index if current_user&.can?(:manage_taxonomies) @@ -11,6 +16,22 @@ class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController end end + def approve + authorize :tag, :review? + + tag = Tag.find(params[:id]) + tag.update(trendable: true, reviewed_at: Time.now.utc) + render json: tag, serializer: REST::Admin::TagSerializer + end + + def reject + authorize :tag, :review? + + tag = Tag.find(params[:id]) + tag.update(trendable: false, reviewed_at: Time.now.utc) + render json: tag, serializer: REST::Admin::TagSerializer + end + private def enabled? diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index db5a866f2..3ee35d141 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -11,15 +11,7 @@ class Auth::SetupController < ApplicationController skip_before_action :require_functional! - def show - flash.now[:notice] = begin - if @user.pending? - I18n.t('devise.registrations.signed_up_but_pending') - else - I18n.t('devise.registrations.signed_up_but_unconfirmed') - end - end - end + def show; end def update # This allows updating the e-mail without entering a password as is required @@ -27,14 +19,13 @@ class Auth::SetupController < ApplicationController # that were not confirmed yet if @user.update(user_params) - redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions') + @user.resend_confirmation_instructions unless @user.confirmed? + redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent') else render :show end end - helper_method :missing_email? - private def require_unconfirmed_or_pending! @@ -53,10 +44,6 @@ class Auth::SetupController < ApplicationController params.require(:user).permit(:email) end - def missing_email? - truthy_param?(:missing_email) - end - def set_pack use_pack 'auth' end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2cac2de59..1228ce36c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -117,6 +117,10 @@ module ApplicationHelper content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end + def check_icon + content_tag(:svg, tag(:path, 'fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor') + end + def visibility_icon(status) if status.public_visibility? fa_icon('globe', title: I18n.t('statuses.visibilities.public')) diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap index f8385357a..fbd44ecc5 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap @@ -3,6 +3,8 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = ` <div className="account__avatar-overlay" + onMouseEnter={[Function]} + onMouseLeave={[Function]} style={ { "height": 46, @@ -15,8 +17,6 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = ` > <div className="account__avatar" - onMouseEnter={[Function]} - onMouseLeave={[Function]} style={ { "height": "36px", @@ -35,8 +35,6 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = ` > <div className="account__avatar" - onMouseEnter={[Function]} - onMouseLeave={[Function]} style={ { "height": "24px", diff --git a/app/javascript/mastodon/components/animated_number.jsx b/app/javascript/mastodon/components/animated_number.jsx deleted file mode 100644 index ce688f04f..000000000 --- a/app/javascript/mastodon/components/animated_number.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ShortNumber from 'mastodon/components/short_number'; -import TransitionMotion from 'react-motion/lib/TransitionMotion'; -import spring from 'react-motion/lib/spring'; -import { reduceMotion } from 'mastodon/initial_state'; - -const obfuscatedCount = count => { - if (count < 0) { - return 0; - } else if (count <= 1) { - return count; - } else { - return '1+'; - } -}; - -export default class AnimatedNumber extends React.PureComponent { - - static propTypes = { - value: PropTypes.number.isRequired, - obfuscate: PropTypes.bool, - }; - - state = { - direction: 1, - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.value > this.props.value) { - this.setState({ direction: 1 }); - } else if (nextProps.value < this.props.value) { - this.setState({ direction: -1 }); - } - } - - willEnter = () => { - const { direction } = this.state; - - return { y: -1 * direction }; - }; - - willLeave = () => { - const { direction } = this.state; - - return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) }; - }; - - render () { - const { value, obfuscate } = this.props; - const { direction } = this.state; - - if (reduceMotion) { - return obfuscate ? obfuscatedCount(value) : <ShortNumber value={value} />; - } - - const styles = [{ - key: `${value}`, - data: value, - style: { y: spring(0, { damping: 35, stiffness: 400 }) }, - }]; - - return ( - <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}> - {items => ( - <span className='animated-number'> - {items.map(({ key, data, style }) => ( - <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span> - ))} - </span> - )} - </TransitionMotion> - ); - } - -} diff --git a/app/javascript/mastodon/components/animated_number.tsx b/app/javascript/mastodon/components/animated_number.tsx new file mode 100644 index 000000000..1673ff41b --- /dev/null +++ b/app/javascript/mastodon/components/animated_number.tsx @@ -0,0 +1,58 @@ +import React, { useCallback, useState } from 'react'; +import ShortNumber from './short_number'; +import { TransitionMotion, spring } from 'react-motion'; +import { reduceMotion } from '../initial_state'; + +const obfuscatedCount = (count: number) => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + +type Props = { + value: number; + obfuscate?: boolean; +} +export const AnimatedNumber: React.FC<Props> = ({ + value, + obfuscate, +})=> { + const [previousValue, setPreviousValue] = useState(value); + const [direction, setDirection] = useState<1|-1>(1); + + if (previousValue !== value) { + setPreviousValue(value); + setDirection(value > previousValue ? 1 : -1); + } + + const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]); + const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]); + + if (reduceMotion) { + return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />; + } + + const styles = [{ + key: `${value}`, + data: value, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }]; + + return ( + <TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}> + {items => ( + <span className='animated-number'> + {items.map(({ key, data, style }) => ( + <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span> + ))} + </span> + )} + </TransitionMotion> + ); +}; + +export default AnimatedNumber; diff --git a/app/javascript/mastodon/components/avatar_overlay.jsx b/app/javascript/mastodon/components/avatar_overlay.jsx deleted file mode 100644 index 034e8ba56..000000000 --- a/app/javascript/mastodon/components/avatar_overlay.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { autoPlayGif } from '../initial_state'; -import Avatar from './avatar'; - -export default class AvatarOverlay extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - friend: ImmutablePropTypes.map.isRequired, - animate: PropTypes.bool, - size: PropTypes.number, - baseSize: PropTypes.number, - overlaySize: PropTypes.number, - }; - - static defaultProps = { - animate: autoPlayGif, - size: 46, - baseSize: 36, - overlaySize: 24, - }; - - state = { - hovering: false, - }; - - handleMouseEnter = () => { - if (this.props.animate) return; - this.setState({ hovering: true }); - }; - - handleMouseLeave = () => { - if (this.props.animate) return; - this.setState({ hovering: false }); - }; - - render() { - const { account, friend, animate, size, baseSize, overlaySize } = this.props; - const { hovering } = this.state; - - return ( - <div className='account__avatar-overlay' style={{ width: size, height: size }}> - <div className='account__avatar-overlay-base'><Avatar animate={hovering || animate} account={account} size={baseSize} /></div> - <div className='account__avatar-overlay-overlay'><Avatar animate={hovering || animate} account={friend} size={overlaySize} /></div> - </div> - ); - } - -} diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx new file mode 100644 index 000000000..5c65a928c --- /dev/null +++ b/app/javascript/mastodon/components/avatar_overlay.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import type { Account } from '../../types/resources'; +import { useHovering } from '../../hooks/useHovering'; +import { autoPlayGif } from '../initial_state'; + +type Props = { + account: Account; + friend: Account; + size?: number; + baseSize?: number; + overlaySize?: number; +}; + +export const AvatarOverlay: React.FC<Props> = ({ + account, + friend, + size = 46, + baseSize = 36, + overlaySize = 24, +}) => { + const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif); + const accountSrc = hovering ? account?.get('avatar') : account?.get('avatar_static'); + const friendSrc = hovering ? friend?.get('avatar') : friend?.get('avatar_static'); + + return ( + <div + className='account__avatar-overlay' style={{ width: size, height: size }} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <div className='account__avatar-overlay-base'> + <div + className='account__avatar' + style={{ width: `${baseSize}px`, height: `${baseSize}px` }} + > + {accountSrc && <img src={accountSrc} alt={account?.get('acct')} />} + </div> + </div> + <div className='account__avatar-overlay-overlay'> + <div + className='account__avatar' + style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }} + > + {friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />} + </div> + </div> + </div> + ); +}; + +export default AvatarOverlay; diff --git a/app/javascript/mastodon/components/gifv.jsx b/app/javascript/mastodon/components/gifv.jsx deleted file mode 100644 index 1ce7e7c29..000000000 --- a/app/javascript/mastodon/components/gifv.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class GIFV extends React.PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string, - lang: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - onClick: PropTypes.func, - }; - - state = { - loading: true, - }; - - handleLoadedData = () => { - this.setState({ loading: false }); - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.src !== this.props.src) { - this.setState({ loading: true }); - } - } - - handleClick = e => { - const { onClick } = this.props; - - if (onClick) { - e.stopPropagation(); - onClick(); - } - }; - - render () { - const { src, width, height, alt, lang } = this.props; - const { loading } = this.state; - - return ( - <div className='gifv' style={{ position: 'relative' }}> - {loading && ( - <canvas - width={width} - height={height} - role='button' - tabIndex={0} - aria-label={alt} - title={alt} - lang={lang} - onClick={this.handleClick} - /> - )} - - <video - src={src} - role='button' - tabIndex={0} - aria-label={alt} - title={alt} - lang={lang} - muted - loop - autoPlay - playsInline - onClick={this.handleClick} - onLoadedData={this.handleLoadedData} - style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }} - /> - </div> - ); - } - -} diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx new file mode 100644 index 000000000..8968170c5 --- /dev/null +++ b/app/javascript/mastodon/components/gifv.tsx @@ -0,0 +1,68 @@ +import React, { useCallback, useState } from 'react'; + +type Props = { + src: string; + key: string; + alt?: string; + lang?: string; + width: number; + height: number; + onClick?: () => void; +} + +export const GIFV: React.FC<Props> = ({ + src, + alt, + lang, + width, + height, + onClick, +})=> { + const [loading, setLoading] = useState(true); + + const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => { + setLoading(false); + }, [setLoading]); + + const handleClick: React.MouseEventHandler = useCallback((e) => { + if (onClick) { + e.stopPropagation(); + onClick(); + } + }, [onClick]); + + return ( + <div className='gifv' style={{ position: 'relative' }}> + {loading && ( + <canvas + width={width} + height={height} + role='button' + tabIndex={0} + aria-label={alt} + title={alt} + lang={lang} + onClick={handleClick} + /> + )} + + <video + src={src} + role='button' + tabIndex={0} + aria-label={alt} + title={alt} + lang={lang} + muted + loop + autoPlay + playsInline + onClick={handleClick} + onLoadedData={handleLoadedData} + style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }} + /> + </div> + ); +}; + +export default GIFV; diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 923dc892d..60a77a39c 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -541,7 +541,7 @@ class Status extends ImmutablePureComponent { expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} onTranslate={this.handleTranslate} - collapsable + collapsible onCollapsedToggle={this.handleCollapsedToggle} /> diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index fb953b9dd..60f820bc5 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -65,7 +65,7 @@ class StatusContent extends React.PureComponent { onExpandedToggle: PropTypes.func, onTranslate: PropTypes.func, onClick: PropTypes.func, - collapsable: PropTypes.bool, + collapsible: PropTypes.bool, onCollapsedToggle: PropTypes.func, languages: ImmutablePropTypes.map, intl: PropTypes.object, @@ -112,10 +112,10 @@ class StatusContent extends React.PureComponent { } if (status.get('collapsed', null) === null && onCollapsedToggle) { - const { collapsable, onClick } = this.props; + const { collapsible, onClick } = this.props; const collapsed = - collapsable + collapsible && onClick && node.clientHeight > MAX_HEIGHT && status.get('spoiler_text').length === 0; diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index d0dbffe65..11f2790bf 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -165,7 +165,7 @@ class Conversation extends ImmutablePureComponent { onClick={this.handleClick} expanded={!lastStatus.get('hidden')} onExpandedToggle={this.handleShowMore} - collapsable + collapsible /> {lastStatus.get('media_attachments').size > 0 && ( diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index b547741f7..900b19c31 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -69,6 +69,7 @@ const messages = defineMessages({ redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, + statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, @@ -166,13 +167,14 @@ const truncate = (str, num) => { } }; -const titleFromStatus = status => { +const titleFromStatus = (intl, status) => { const displayName = status.getIn(['account', 'display_name']); const username = status.getIn(['account', 'username']); - const prefix = displayName.trim().length === 0 ? username : displayName; + const user = displayName.trim().length === 0 ? username : displayName; const text = status.get('search_index'); + const attachmentCount = status.get('media_attachments').size; - return `${prefix}: "${truncate(text, 30)}"`; + return text ? `${user}: "${truncate(text, 30)}"` : intl.formatMessage(messages.statusTitleWithAttachments, { user, attachmentCount }); }; class Status extends ImmutablePureComponent { @@ -670,7 +672,7 @@ class Status extends ImmutablePureComponent { </ScrollContainer> <Helmet> - <title>{titleFromStatus(status)}</title> + <title>{titleFromStatus(intl, status)}</title> <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} /> </Helmet> </Column> diff --git a/app/javascript/mastodon/features/ui/components/filter_modal.jsx b/app/javascript/mastodon/features/ui/components/filter_modal.jsx index 32ebaf7b7..8d77fb3df 100644 --- a/app/javascript/mastodon/features/ui/components/filter_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/filter_modal.jsx @@ -131,4 +131,4 @@ class FilterModal extends ImmutablePureComponent { } -export default connect(injectIntl(FilterModal)); +export default connect()(injectIntl(FilterModal)); diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx index 11c4c5237..2a1e4c8bb 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx @@ -383,7 +383,7 @@ class FocalPointModal extends ImmutablePureComponent { {focals && ( <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}> {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />} - {media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />} + {media.get('type') === 'gifv' && <GIFV src={media.get('url')} key={media.get('url')} width={width} height={height} />} <div className='focal-point__preview'> <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong> diff --git a/app/javascript/mastodon/features/ui/components/media_modal.jsx b/app/javascript/mastodon/features/ui/components/media_modal.jsx index 52bd75453..ec6ddc0e1 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.jsx @@ -186,7 +186,7 @@ class MediaModal extends ImmutablePureComponent { src={image.get('url')} width={width} height={height} - key={image.get('preview_url')} + key={image.get('url')} alt={image.get('description')} lang={language} onClick={this.toggleNavigation} diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 1351945eb..6d6683808 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -3733,6 +3733,10 @@ "id": "status.show_less_all" }, { + "defaultMessage": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}", + "id": "status.title.with_attachments" + }, + { "defaultMessage": "Detailed conversation view", "id": "status.detailed_status" }, @@ -4354,5 +4358,22 @@ } ], "path": "app/javascript/mastodon/features/video/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "That username is taken. Try another", + "id": "username.taken" + }, + { + "defaultMessage": "Password confirmation exceeds the maximum password length", + "id": "password_confirmation.exceeds_maxlength" + }, + { + "defaultMessage": "Password confirmation does not match", + "id": "password_confirmation.mismatching" + } + ], + "path": "app/javascript/packs/public.json" } ] \ No newline at end of file diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index ae2d5a999..31fa3ca3a 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -443,6 +443,8 @@ "notifications_permission_banner.enable": "Enable desktop notifications", "notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.", "notifications_permission_banner.title": "Never miss a thing", + "password_confirmation.exceeds_maxlength": "Password confirmation exceeds the maximum password length", + "password_confirmation.mismatching": "Password confirmation does not match", "picture_in_picture.restore": "Put it back", "poll.closed": "Closed", "poll.refresh": "Refresh", @@ -598,6 +600,7 @@ "status.show_more": "Show more", "status.show_more_all": "Show more for all", "status.show_original": "Show original", + "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}", "status.translate": "Translate", "status.translated_from_with": "Translated from {lang} using {provider}", "status.uncached_media_warning": "Not available", @@ -650,6 +653,7 @@ "upload_modal.preview_label": "Preview ({ratio})", "upload_progress.label": "Uploading...", "upload_progress.processing": "Processing…", + "username.taken": "That username is taken. Try another", "video.close": "Close video", "video.download": "Download file", "video.exit_fullscreen": "Exit full screen", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 0085563d2..886976823 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -597,6 +597,7 @@ "status.show_more": "Mostrar más", "status.show_more_all": "Mostrar más para todo", "status.show_original": "Mostrar original", + "status.title.with_attachments": "{user} publicó {attachmentCount, plural, one {un archivo adjunto} other {{attachmentCount} archivos adjuntos}}", "status.translate": "Traducir", "status.translated_from_with": "Traducido del {lang} usando {provider}", "status.uncached_media_warning": "No disponible", diff --git a/app/javascript/packs/public.jsx b/app/javascript/packs/public.jsx index ad6bf237f..606ddc3bf 100644 --- a/app/javascript/packs/public.jsx +++ b/app/javascript/packs/public.jsx @@ -4,6 +4,15 @@ import ready from '../mastodon/ready'; import { start } from '../mastodon/common'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; import 'cocoon-js-vanilla'; +import axios from 'axios'; +import { throttle } from 'lodash'; +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' }, + passwordExceedsLength: { id: 'password_confirmation.exceeds_maxlength', defaultMessage: 'Password confirmation exceeds the maximum password length' }, + passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' }, +}); start(); @@ -13,7 +22,7 @@ function main() { const { delegate } = require('@rails/ujs'); const emojify = require('../mastodon/features/emoji/emoji').default; const { getLocale } = require('../mastodon/locales'); - const { messages } = getLocale(); + const { localeData } = getLocale(); const React = require('react'); const ReactDOM = require('react-dom'); const { createBrowserHistory } = require('history'); @@ -58,6 +67,11 @@ function main() { hour12: false, }); + const formatMessage = ({ id, defaultMessage }, values) => { + const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale); + return messageFormat.format(values); + }; + [].forEach.call(document.querySelectorAll('.emojify'), (content) => { content.innerHTML = emojify(content.innerHTML); }); @@ -77,7 +91,7 @@ function main() { date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear(); }; - const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale); + const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale); [].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => { const datetime = new Date(content.getAttribute('datetime')); @@ -103,7 +117,7 @@ function main() { const timeGiven = content.getAttribute('datetime').includes('T'); content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime); content.textContent = timeAgoString({ - formatMessage: ({ id, defaultMessage }, values) => (new IntlMessageFormat(messages[id] || defaultMessage, locale)).format(values), + formatMessage, formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date), }, datetime, now, now.getFullYear(), timeGiven); }); @@ -133,17 +147,19 @@ function main() { scrollToDetailedStatus(); } - 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'); - 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) { - confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format()); + delegate(document, '#user_account_attributes_username', 'input', throttle(() => { + const username = document.getElementById('user_account_attributes_username'); + + if (username.value && username.value.length > 0) { + axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => { + username.setCustomValidity(formatMessage(messages.usernameTaken)); + }).catch(() => { + username.setCustomValidity(''); + }); } else { - confirmation.setCustomValidity(''); + username.setCustomValidity(''); } - }); + }, 500, { leading: false, trailing: true })); delegate(document, '#user_password,#user_password_confirmation', 'input', () => { const password = document.getElementById('user_password'); @@ -151,9 +167,9 @@ function main() { if (!confirmation) return; 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()); + confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength)); } else if (password.value && password.value !== confirmation.value) { - confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format()); + confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch)); } else { confirmation.setCustomValidity(''); } @@ -167,10 +183,10 @@ function main() { if (statusEl.dataset.spoiler === 'expanded') { statusEl.dataset.spoiler = 'folded'; - this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format(); + this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format(); } else { statusEl.dataset.spoiler = 'expanded'; - this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format(); + this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format(); } return false; @@ -178,7 +194,7 @@ function main() { [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => { const statusEl = spoilerLink.parentNode.parentNode; - const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more'); + const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more'); spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format(); }); }); diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 7d4bde5e9..dc52bcd87 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -1112,3 +1112,89 @@ code { white-space: nowrap; } } + +.progress-tracker { + display: flex; + align-items: center; + padding-bottom: 30px; + margin-bottom: 30px; + + li { + flex: 0 0 auto; + position: relative; + } + + .separator { + height: 2px; + background: $ui-base-lighter-color; + flex: 1 1 auto; + + &.completed { + background: $highlight-text-color; + } + } + + .circle { + box-sizing: border-box; + position: relative; + width: 30px; + height: 30px; + border-radius: 50%; + border: 2px solid $ui-base-lighter-color; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 16px; + } + } + + .label { + position: absolute; + font-size: 14px; + font-weight: 500; + color: $secondary-text-color; + padding-top: 10px; + text-align: center; + width: 100px; + left: 50%; + transform: translateX(-50%); + } + + li:first-child .label { + left: auto; + inset-inline-start: 0; + text-align: start; + transform: none; + } + + li:last-child .label { + left: auto; + inset-inline-end: 0; + text-align: end; + transform: none; + } + + .active .circle { + border-color: $highlight-text-color; + + &::before { + content: ''; + width: 10px; + height: 10px; + border-radius: 50%; + background: $highlight-text-color; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } + } + + .completed .circle { + border-color: $highlight-text-color; + background: $highlight-text-color; + } +} diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index ab73826ab..c428fd30d 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -14,7 +14,7 @@ class NotificationMailer < ApplicationMailer locale_for_account(@me) do thread_by_conversation(@status.conversation) - mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct) + mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct) end end @@ -25,7 +25,7 @@ class NotificationMailer < ApplicationMailer return unless @me.user.functional? locale_for_account(@me) do - mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) + mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) end end @@ -38,7 +38,7 @@ class NotificationMailer < ApplicationMailer locale_for_account(@me) do thread_by_conversation(@status.conversation) - mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct) + mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct) end end @@ -51,7 +51,7 @@ class NotificationMailer < ApplicationMailer locale_for_account(@me) do thread_by_conversation(@status.conversation) - mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct) + mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct) end end @@ -62,7 +62,7 @@ class NotificationMailer < ApplicationMailer return unless @me.user.functional? locale_for_account(@me) do - mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) + mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) end end diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index 1666ea883..55d34e85c 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -55,7 +55,7 @@ class AccountFilter when 'by_domain' Account.where(domain: value.to_s.strip) when 'username' - Account.matches_username(value.to_s.strip) + Account.matches_username(value.to_s.strip.delete_prefix('@')) when 'display_name' Account.matches_display_name(value.to_s.strip) when 'email' diff --git a/app/models/preview_card_provider.rb b/app/models/preview_card_provider.rb index 1dd95fc91..9f5f6d3cb 100644 --- a/app/models/preview_card_provider.rb +++ b/app/models/preview_card_provider.rb @@ -18,6 +18,7 @@ # class PreviewCardProvider < ApplicationRecord + include Paginable include DomainNormalizable include Attachmentable diff --git a/app/serializers/rest/admin/trends/link_serializer.rb b/app/serializers/rest/admin/trends/link_serializer.rb new file mode 100644 index 000000000..c93e6c178 --- /dev/null +++ b/app/serializers/rest/admin/trends/link_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::Admin::Trends::LinkSerializer < REST::Trends::LinkSerializer + attributes :id, :requires_review + + def requires_review + object.requires_review? + end +end diff --git a/app/serializers/rest/admin/trends/links/preview_card_provider_serializer.rb b/app/serializers/rest/admin/trends/links/preview_card_provider_serializer.rb new file mode 100644 index 000000000..fba0259fb --- /dev/null +++ b/app/serializers/rest/admin/trends/links/preview_card_provider_serializer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class REST::Admin::Trends::Links::PreviewCardProviderSerializer < ActiveModel::Serializer + attributes :id, :domain, :trendable, :reviewed_at, + :requested_review_at, :requires_review + + def requires_review + object.requires_review? + end +end diff --git a/app/serializers/rest/admin/trends/status_serializer.rb b/app/serializers/rest/admin/trends/status_serializer.rb new file mode 100644 index 000000000..e46be30ab --- /dev/null +++ b/app/serializers/rest/admin/trends/status_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::Admin::Trends::StatusSerializer < REST::StatusSerializer + attributes :requires_review + + def requires_review + object.requires_review? + end +end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 069f370cf..ad9e6e3d6 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -7,6 +7,7 @@ class NotifyService < BaseService admin.report admin.sign_up update + poll ).freeze def call(recipient, type, activity) diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index 0d8fd800f..4df0f95d5 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -5,6 +5,8 @@ = render partial: 'shared/og', locals: { description: description_for_sign_up } = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { novalidate: false }) do |f| + = render 'auth/shared/progress', stage: 'details' + %h1.title= t('auth.sign_up.title', domain: site_hostname) %p.lead= t('auth.sign_up.preamble') @@ -18,7 +20,7 @@ .fields-group = f.simple_fields_for :account do |ff| = ff.input :display_name, wrapper: :with_label, label: false, required: false, input_html: { 'aria-label': t('simple_form.labels.defaults.display_name'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.display_name') } - = ff.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false + = ff.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}" = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' }, hint: false = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last }, hint: false = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password' }, hint: false @@ -26,9 +28,11 @@ = f.input :website, as: :url, wrapper: :with_label, label: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label': t('simple_form.labels.defaults.honeypot', label: 'Website'), autocomplete: 'off' } - if approved_registrations? && !@invite.present? + %p.lead= t('auth.sign_up.manual_review', domain: site_hostname) + .fields-group = f.simple_fields_for :invite_request, resource.invite_request || resource.build_invite_request do |invite_request_fields| - = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text + = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text, label: false, hint: false = hidden_field_tag :accept, params[:accept] diff --git a/app/views/auth/registrations/rules.html.haml b/app/views/auth/registrations/rules.html.haml index 8e7a90cbe..aa16ab957 100644 --- a/app/views/auth/registrations/rules.html.haml +++ b/app/views/auth/registrations/rules.html.haml @@ -5,6 +5,8 @@ = render partial: 'shared/og', locals: { description: description_for_sign_up } .simple_form + = render 'auth/shared/progress', stage: 'rules' + %h1.title= t('auth.rules.title') %p.lead= t('auth.rules.preamble', domain: site_hostname) diff --git a/app/views/auth/setup/show.html.haml b/app/views/auth/setup/show.html.haml index 1a6611ceb..913b0c913 100644 --- a/app/views/auth/setup/show.html.haml +++ b/app/views/auth/setup/show.html.haml @@ -1,20 +1,22 @@ - content_for :page_title do = t('auth.setup.title') -- if missing_email? - = simple_form_for(@user, url: auth_setup_path) do |f| - = render 'shared/error_messages', object: @user += simple_form_for(@user, url: auth_setup_path) do |f| + = render 'auth/shared/progress', stage: 'confirm' - .fields-group - %p.hint= t('auth.setup.email_below_hint_html') + %h1.title= t('auth.setup.title') + %p.lead= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email)) - .fields-group - = f.input :email, required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'off' } + = render 'shared/error_messages', object: @user - .actions - = f.submit t('admin.accounts.change_email.label'), class: 'button' -- else - .simple_form - %p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email)) + %p.lead + %strong= t('auth.setup.link_not_received') + %p.lead= t('auth.setup.email_below_hint_html') + + .fields-group + = f.input :email, required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'off' } + + .actions + = f.submit t('auth.resend_confirmation'), class: 'button' .form-footer= render 'auth/shared/links' diff --git a/app/views/auth/shared/_links.html.haml b/app/views/auth/shared/_links.html.haml index f078e2f7e..757ef0a09 100644 --- a/app/views/auth/shared/_links.html.haml +++ b/app/views/auth/shared/_links.html.haml @@ -14,5 +14,5 @@ - if controller_name != 'confirmations' && (!user_signed_in? || !current_user.confirmed? || current_user.unconfirmed_email.present?) %li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path - - if user_signed_in? && controller_name != 'setup' + - if user_signed_in? %li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete } diff --git a/app/views/auth/shared/_progress.html.haml b/app/views/auth/shared/_progress.html.haml new file mode 100644 index 000000000..578f62fa9 --- /dev/null +++ b/app/views/auth/shared/_progress.html.haml @@ -0,0 +1,25 @@ +- progress_index = { rules: 0, details: 1, confirm: 2 }[stage.to_sym] + +%ol.progress-tracker + %li{ class: progress_index.positive? ? 'completed' : 'active' } + .circle + - if progress_index.positive? + = check_icon + .label= t('auth.progress.rules') + %li.separator{ class: progress_index.positive? ? 'completed' : nil } + %li{ class: [progress_index > 1 && 'completed', progress_index == 1 && 'active'] } + .circle + - if progress_index > 1 + = check_icon + .label= t('auth.progress.details') + %li.separator{ class: progress_index > 1 ? 'completed' : nil } + %li{ class: [progress_index > 2 && 'completed', progress_index == 2 && 'active'] } + .circle + - if progress_index > 2 + = check_icon + .label= t('auth.progress.confirm') + - if approved_registrations? + %li.separator{ class: progress_index > 2 ? 'completed' : nil } + %li + .circle + .label= t('auth.progress.review') |