Merge commit 'c4eec632b92c800ae38dba111c4c76e63bb1c0de' into glitch-soc/merge-upstream
This commit is contained in:
commit
b6abf7a69f
@ -76,6 +76,26 @@
|
||||
}
|
||||
|
||||
.emptyMessage {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.loadingIndicator {
|
||||
--spinner-size: 20px;
|
||||
|
||||
position: relative;
|
||||
display: block;
|
||||
width: var(--spinner-size);
|
||||
height: var(--spinner-size);
|
||||
overflow: hidden;
|
||||
|
||||
& :global(.circular-progress) {
|
||||
width: var(--spinner-size);
|
||||
height: var(--spinner-size);
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,8 @@ import { matchWidth } from 'mastodon/components/dropdown/utils';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { useOnClickOutside } from 'mastodon/hooks/useOnClickOutside';
|
||||
|
||||
import { LoadingIndicator } from '../loading_indicator';
|
||||
|
||||
import classes from './combobox.module.scss';
|
||||
import { FormFieldWrapper } from './form_field_wrapper';
|
||||
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
||||
@ -531,6 +533,7 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
||||
<div {...props} className={classNames(classes.popover, placement)}>
|
||||
<StatusMessageWrapper
|
||||
showStatus={showStatusMessageInMenu}
|
||||
isLoading={isLoading}
|
||||
status={statusMessage}
|
||||
>
|
||||
{hasGroups ? (
|
||||
@ -592,10 +595,20 @@ Combobox.displayName = 'Combobox';
|
||||
const StatusMessageWrapper: React.FC<{
|
||||
showStatus: boolean;
|
||||
status: string;
|
||||
isLoading: boolean;
|
||||
children: React.ReactNode;
|
||||
}> = ({ showStatus, status, children }) => {
|
||||
}> = ({ showStatus, status, isLoading, children }) => {
|
||||
if (showStatus) {
|
||||
return <span className={classes.emptyMessage}>{status}</span>;
|
||||
return (
|
||||
<span className={classes.emptyMessage}>
|
||||
{isLoading && (
|
||||
<span className={classes.loadingIndicator}>
|
||||
<LoadingIndicator role='none' />
|
||||
</span>
|
||||
)}
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
.wrapper {
|
||||
--diameter: 20px;
|
||||
--padding: 2px;
|
||||
--transition: 0.2s ease-in-out;
|
||||
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: calc(var(--diameter) + var(--padding) * 2);
|
||||
}
|
||||
|
||||
.input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
@ -9,10 +19,6 @@
|
||||
}
|
||||
|
||||
.toggle {
|
||||
--diameter: 20px;
|
||||
--padding: 2px;
|
||||
--transition: 0.2s ease-in-out;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 9999px;
|
||||
@ -68,8 +74,3 @@
|
||||
:global([dir='rtl']) .input:checked + .toggle::before {
|
||||
transform: translateX(calc(-1 * (var(--diameter) - (var(--padding) * 2))));
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@ -31,20 +31,17 @@ ToggleField.displayName = 'ToggleField';
|
||||
|
||||
export const Toggle = forwardRef<HTMLInputElement, Props>(
|
||||
({ className, size, ...otherProps }, ref) => (
|
||||
<span className={classes.wrapper}>
|
||||
<span
|
||||
className={classes.wrapper}
|
||||
style={{ '--diameter': size ? `${size}px` : undefined } as CSSProperties}
|
||||
>
|
||||
<input
|
||||
{...otherProps}
|
||||
type='checkbox'
|
||||
className={classes.input}
|
||||
ref={ref}
|
||||
/>
|
||||
<span
|
||||
className={classNames(classes.toggle, className)}
|
||||
style={
|
||||
{ '--diameter': size ? `${size}px` : undefined } as CSSProperties
|
||||
}
|
||||
hidden
|
||||
/>
|
||||
<span className={classNames(classes.toggle, className)} hidden />
|
||||
</span>
|
||||
),
|
||||
);
|
||||
|
||||
@ -331,6 +331,10 @@ const TopicField: React.FC = () => {
|
||||
[topic],
|
||||
);
|
||||
|
||||
const isCurrentTopicOnlySuggestion =
|
||||
tags.length === 1 && tags[0]?.id === 'new';
|
||||
const hideTagSuggestions = !tags.length || isCurrentTopicOnlySuggestion;
|
||||
|
||||
return (
|
||||
<ComboboxField
|
||||
required={false}
|
||||
@ -369,7 +373,7 @@ const TopicField: React.FC = () => {
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
suppressMenu={!tags.length}
|
||||
suppressMenu={hideTagSuggestions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,8 +7,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { NonceProvider } from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
|
||||
import { injectIntl } from '@/mastodon/components/intl';
|
||||
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
@ -7,8 +7,6 @@ import { useParams, useHistory, Link } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { fetchList } from 'mastodon/actions/lists';
|
||||
@ -20,7 +18,11 @@ import { Avatar } from 'mastodon/components/avatar';
|
||||
import { AvatarGroup } from 'mastodon/components/avatar_group';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { SelectField, TextInputField } from 'mastodon/components/form_fields';
|
||||
import {
|
||||
SelectField,
|
||||
TextInputField,
|
||||
Toggle,
|
||||
} from 'mastodon/components/form_fields';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import type { List } from 'mastodon/models/list';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
|
||||
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
|
||||
@ -3,7 +3,7 @@ import { PureComponent } from 'react';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
|
||||
|
||||
export default class SettingToggle extends PureComponent {
|
||||
|
||||
@ -27,7 +27,7 @@ export default class SettingToggle extends PureComponent {
|
||||
|
||||
return (
|
||||
<div className='setting-toggle'>
|
||||
<Toggle disabled={disabled} id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
|
||||
<Toggle disabled={disabled} id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} size={16} />
|
||||
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -6,8 +6,6 @@ import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
|
||||
@ -16,7 +14,11 @@ import { closeOnboarding } from 'mastodon/actions/onboarding';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { TextAreaField, TextInputField } from 'mastodon/components/form_fields';
|
||||
import {
|
||||
TextAreaField,
|
||||
TextInputField,
|
||||
Toggle,
|
||||
} from 'mastodon/components/form_fields';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
||||
@ -6,8 +6,7 @@ import type { Map } from 'immutable';
|
||||
import { OrderedSet } from 'immutable';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
|
||||
import { fetchAccount } from 'mastodon/actions/accounts';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import type { Status } from 'mastodon/models/status';
|
||||
|
||||
@ -13,12 +13,12 @@
|
||||
"about.not_available": "Þessar upplýsingar hafa ekki verið gerðar aðgengilegar á þessum netþjóni.",
|
||||
"about.powered_by": "Dreifhýstur samskiptamiðill keyrður með {mastodon}",
|
||||
"about.rules": "Reglur netþjónsins",
|
||||
"account.account_note_header": "Einkaminnispunktur",
|
||||
"account.account_note_header": "Mínir minnispunktar",
|
||||
"account.activity": "Virkni",
|
||||
"account.add_note": "Bæta við einkaminnispunkti",
|
||||
"account.add_note": "Bæta við persónulegum minnispunkti",
|
||||
"account.add_or_remove_from_list": "Bæta við eða fjarlægja af listum",
|
||||
"account.badges.admin": "Stjóri",
|
||||
"account.badges.blocked": "Lokað á",
|
||||
"account.badges.blocked": "Útilokað",
|
||||
"account.badges.bot": "Yrki",
|
||||
"account.badges.domain_blocked": "Útilokað lén",
|
||||
"account.badges.group": "Hópur",
|
||||
@ -28,13 +28,13 @@
|
||||
"account.block_domain": "Útiloka lénið {domain}",
|
||||
"account.block_short": "Útiloka",
|
||||
"account.blocked": "Útilokaður",
|
||||
"account.blocking": "Útilokun",
|
||||
"account.blocking": "Útilokað",
|
||||
"account.cancel_follow_request": "Taka fylgjendabeiðni til baka",
|
||||
"account.copy": "Afrita tengil í notandasnið",
|
||||
"account.direct": "Einkaspjall við @{name}",
|
||||
"account.disable_notifications": "Hætta að láta mig vita þegar @{name} sendir inn",
|
||||
"account.domain_blocking": "Útiloka lén",
|
||||
"account.edit_note": "Breyta einkaminnispunkti",
|
||||
"account.domain_blocking": "Útilokað lén",
|
||||
"account.edit_note": "Breyta persónulegum minnispunkti",
|
||||
"account.edit_profile": "Breyta notandasniði",
|
||||
"account.edit_profile_short": "Breyta",
|
||||
"account.enable_notifications": "Láta mig vita þegar @{name} sendir inn",
|
||||
@ -55,8 +55,8 @@
|
||||
"account.filters.replies_toggle": "Birta svör",
|
||||
"account.follow": "Fylgjast með",
|
||||
"account.follow_back": "Fylgjast með til baka",
|
||||
"account.follow_back_short": "Fylgjast með til baka",
|
||||
"account.follow_request": "Beiðni um að fylgjast með",
|
||||
"account.follow_back_short": "Fylgja líka",
|
||||
"account.follow_request": "Beiðni um að fylgja",
|
||||
"account.follow_request_cancel": "Hætta við beiðni",
|
||||
"account.follow_request_cancel_short": "Hætta við",
|
||||
"account.follow_request_short": "Beiðni",
|
||||
@ -90,7 +90,7 @@
|
||||
"account.menu.note.description": "Einungis sýnilegt þér",
|
||||
"account.menu.open_original_page": "Skoða á {domain}",
|
||||
"account.menu.remove_follower": "Fjarlægja fylgjanda",
|
||||
"account.menu.report": "Kæra aðgang",
|
||||
"account.menu.report": "Tilkynna aðgang",
|
||||
"account.menu.share": "Deila…",
|
||||
"account.menu.show_reblogs": "Sýna endurbirtingar í tímalínu",
|
||||
"account.menu.unblock": "Aflétta útilokun aðgangs",
|
||||
@ -102,7 +102,7 @@
|
||||
"account.mute_short": "Þagga niður",
|
||||
"account.muted": "Þaggaður",
|
||||
"account.muting": "Þöggun",
|
||||
"account.mutual": "Þið fylgist með hvor öðrum",
|
||||
"account.mutual": "Þið fylgið hvor öðrum",
|
||||
"account.name.copy": "Afrita kennislóð",
|
||||
"account.name.help.domain": "{domain} er netþjónninn sem hýsir upplýsingasnið um notandann og færslurnar hans.",
|
||||
"account.name.help.domain_self": "{domain} er netþjónninn þinn sem hýsir upplýsingasniðið þitt og færslurnar þínar.",
|
||||
@ -125,7 +125,7 @@
|
||||
"account.remove_from_followers": "Fjarlægja {name} úr fylgjendum",
|
||||
"account.report": "Kæra @{name}",
|
||||
"account.requested_follow": "{name} hefur beðið um að fylgjast með þér",
|
||||
"account.requests_to_follow_you": "Bað um að fylgjast með þér",
|
||||
"account.requests_to_follow_you": "Bað um að fylgja þér",
|
||||
"account.share": "Deila notandasniði fyrir @{name}",
|
||||
"account.show_reblogs": "Sýna endurbirtingar frá @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} færsla} other {{counter} færslur}}",
|
||||
@ -402,7 +402,7 @@
|
||||
"collections.new_collection": "Nýtt safn",
|
||||
"collections.no_collections_yet": "Engin söfn ennþá.",
|
||||
"collections.remove_account": "Fjarlægja",
|
||||
"collections.report_collection": "Kæra þetta safn",
|
||||
"collections.report_collection": "Tilkynna þetta safn",
|
||||
"collections.revoke_collection_inclusion": "Fjarlægja mig úr þessu safni",
|
||||
"collections.revoke_inclusion.confirmation": "Þú varst fjarlægð/ur úr \"{collection}\"",
|
||||
"collections.revoke_inclusion.error": "Upp kom villa, reyndu aftur síðar.",
|
||||
@ -412,8 +412,8 @@
|
||||
"collections.share_short": "Deila",
|
||||
"collections.suggestions.can_not_add": "Er ekki hægt að bæta við",
|
||||
"collections.suggestions.can_not_add_desc": "Þessir aðgangar gætu hafa skráð sig úr almennri birtingu eða gætu verið á netþjónum sem ekki styðja söfn.",
|
||||
"collections.suggestions.must_follow": "Verður fyrst að fylgjast með",
|
||||
"collections.suggestions.must_follow_desc": "Þessir aðgangar yfirfara allar beiðnir um að fylgjast með. Fylgjendur geta bætt þeim í söfn.",
|
||||
"collections.suggestions.must_follow": "Verður fyrst að fylgja",
|
||||
"collections.suggestions.must_follow_desc": "Þessir aðgangar yfirfara allar beiðnir um að fylgja þeim. Fylgjendur geta bætt þeim í söfn.",
|
||||
"collections.topic_hint": "Bættu við myllumerki sem hjálpar öðrum að skilja aðalefni þessa safns.",
|
||||
"collections.topic_special_chars_hint": "Sérstafir verða fjarlægðir við vistun",
|
||||
"collections.unlisted_collections_description": "Þessi birtast ekki öðrum á notandasíðunni þinni. Þeir sem eru með tengil á þau geta séð þau.",
|
||||
@ -508,9 +508,9 @@
|
||||
"confirmations.discard_draft.post.title": "Henda drögum að færslunni þinni?",
|
||||
"confirmations.discard_edit_media.confirm": "Henda",
|
||||
"confirmations.discard_edit_media.message": "Þú ert með óvistaðar breytingar á lýsingu myndefnis eða forskoðunar, henda þeim samt?",
|
||||
"confirmations.follow_to_list.confirm": "Fylgjast með og bæta á lista",
|
||||
"confirmations.follow_to_list.message": "Þú þarft að fylgjast með {name} til að bæta viðkomandi á lista.",
|
||||
"confirmations.follow_to_list.title": "Fylgjast með notanda?",
|
||||
"confirmations.follow_to_list.confirm": "Fylgja og bæta á lista",
|
||||
"confirmations.follow_to_list.message": "Þú þarft að fylgja {name} til að bæta viðkomandi á lista.",
|
||||
"confirmations.follow_to_list.title": "Fylgja notanda?",
|
||||
"confirmations.hide_featured_tab.confirm": "Fela flipa",
|
||||
"confirmations.hide_featured_tab.intro": "Þú getur breytt þessu hvenær sem er í <i>Breyta notandasniði > Stillingar notandasniðsflipa</i>.",
|
||||
"confirmations.hide_featured_tab.message": "Þetta mun fela flipann fyrir notendum á {serverName} og öðrum netþjónum sem nota nýjustu útgáfuna af Mastodon. Birting í öðrum viðmótum gæti verið á ýmsan máta.",
|
||||
@ -536,7 +536,7 @@
|
||||
"confirmations.redraft.message": "Ertu viss um að þú viljir eyða þessari færslu og enduvinna drögin? Eftirlæti og endurbirtingar munu glatast og svör við upprunalegu færslunni munu verða munaðarlaus.",
|
||||
"confirmations.redraft.title": "Eyða og byrja ný drög að færslu?",
|
||||
"confirmations.remove_from_followers.confirm": "Fjarlægja fylgjanda",
|
||||
"confirmations.remove_from_followers.message": "{name} mun hætta að fylgjast með þér. Ertu viss um að þú viljir halda áfram?",
|
||||
"confirmations.remove_from_followers.message": "{name} mun hætta að fylgja þér. Ertu viss um að þú viljir halda áfram?",
|
||||
"confirmations.remove_from_followers.title": "Fjarlægja fylgjanda?",
|
||||
"confirmations.revoke_collection_inclusion.confirm": "Fjarlægja mig",
|
||||
"confirmations.revoke_collection_inclusion.message": "Þessi aðgerð er varanleg og umsjónaraðili safnsins mun ekki geta bætt þér aftur við síðar.",
|
||||
@ -547,9 +547,9 @@
|
||||
"confirmations.unblock.confirm": "Aflétta útilokun",
|
||||
"confirmations.unblock.title": "Aflétta útilokun á {name}?",
|
||||
"confirmations.unfollow.confirm": "Hætta að fylgja",
|
||||
"confirmations.unfollow.title": "Hætta að fylgjast með {name}?",
|
||||
"confirmations.unfollow.title": "Hætta að fylgja {name}?",
|
||||
"confirmations.withdraw_request.confirm": "Taka beiðni til baka",
|
||||
"confirmations.withdraw_request.title": "Taka aftur beiðni um að fylgjast með {name}?",
|
||||
"confirmations.withdraw_request.title": "Taka aftur beiðni um að fylgja {name}?",
|
||||
"content_warning.hide": "Fela færslu",
|
||||
"content_warning.show": "Birta samt",
|
||||
"content_warning.show_more": "Sýna meira",
|
||||
@ -1113,7 +1113,7 @@
|
||||
"report.category.title_account": "notandasnið",
|
||||
"report.category.title_status": "færsla",
|
||||
"report.close": "Lokið",
|
||||
"report.collection_comment": "Hvers vegna viltu kæra þetta safn?",
|
||||
"report.collection_comment": "Afhverju viltu tilkynna þetta safn?",
|
||||
"report.comment.title": "Er eitthvað annað sem þú heldur að við ættum að vita?",
|
||||
"report.forward": "Áframsenda til {target}",
|
||||
"report.forward_hint": "Notandaaðgangurinn er af öðrum vefþjóni. Á einnig að senda nafnlaust afrit af kærunni þangað?",
|
||||
|
||||
@ -410,6 +410,10 @@
|
||||
"collections.search_accounts_max_reached": "Du har lagt till maximalt antal konton",
|
||||
"collections.sensitive": "Känsligt",
|
||||
"collections.share_short": "Dela",
|
||||
"collections.suggestions.can_not_add": "Kan inte läggas till",
|
||||
"collections.suggestions.can_not_add_desc": "Dessa konton kan ha valt bort upptäckt, eller kan de vara på en server som inte stöder samlingar.",
|
||||
"collections.suggestions.must_follow": "Måste följa först",
|
||||
"collections.suggestions.must_follow_desc": "Dessa konton granskar alla följa förfrågningar. Följare kan lägga till dem i samlingar.",
|
||||
"collections.topic_hint": "Lägg till en hashtagg som hjälper andra att förstå huvudämnet i denna samling.",
|
||||
"collections.topic_special_chars_hint": "Specialtecken kommer att tas bort när du sparar",
|
||||
"collections.unlisted_collections_description": "Dessa visas inte på din profil till andra. Vem som helst med länken kan upptäcka dem.",
|
||||
|
||||
@ -1122,7 +1122,7 @@ a.name-tag,
|
||||
text-decoration: none;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:hover {
|
||||
&:where(a):where(:hover, :focus, :active) {
|
||||
color: var(--color-text-brand);
|
||||
}
|
||||
}
|
||||
@ -1131,14 +1131,6 @@ a.name-tag,
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
a.announcements-list__item__title {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -53,6 +53,7 @@ class Collection < ApplicationRecord
|
||||
validates :language, language: { if: :local?, allow_nil: true }
|
||||
validate :tag_is_usable
|
||||
validate :items_do_not_exceed_limit
|
||||
validate :user_does_not_exceed_limit, on: :create
|
||||
|
||||
scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) }
|
||||
scope :with_tag, -> { includes(:tag) }
|
||||
@ -105,4 +106,11 @@ class Collection < ApplicationRecord
|
||||
def items_do_not_exceed_limit
|
||||
errors.add(:collection_items, :too_many, count: MAX_ITEMS) if pending_or_accepted_items.size > MAX_ITEMS
|
||||
end
|
||||
|
||||
def user_does_not_exceed_limit
|
||||
return unless local?
|
||||
|
||||
limit = account.user.role.collection_limit
|
||||
errors.add(:base, :too_many, count: limit) if account.collections.count >= limit
|
||||
end
|
||||
end
|
||||
|
||||
@ -4,15 +4,16 @@
|
||||
#
|
||||
# Table name: user_roles
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# color :string default(""), not null
|
||||
# highlighted :boolean default(FALSE), not null
|
||||
# name :string default(""), not null
|
||||
# permissions :bigint(8) default(0), not null
|
||||
# position :integer default(0), not null
|
||||
# require_2fa :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# id :bigint(8) not null, primary key
|
||||
# collection_limit :integer default(10), not null
|
||||
# color :string default(""), not null
|
||||
# highlighted :boolean default(FALSE), not null
|
||||
# name :string default(""), not null
|
||||
# permissions :bigint(8) default(0), not null
|
||||
# position :integer default(0), not null
|
||||
# require_2fa :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class UserRole < ApplicationRecord
|
||||
@ -104,6 +105,7 @@ class UserRole < ApplicationRecord
|
||||
validates :name, presence: true, unless: :everyone?
|
||||
validates :color, format: { with: CSS_COLORS }, if: :color?
|
||||
validates :position, numericality: { in: (-POSITION_LIMIT..POSITION_LIMIT) }
|
||||
validates :collection_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
|
||||
validate :validate_permissions_elevation
|
||||
validate :validate_position_elevation
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
class REST::RoleSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :permissions, :color, :highlighted
|
||||
|
||||
attribute :collection_limit, if: -> { Mastodon::Feature.collections_enabled? }
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
@ -32,6 +32,14 @@
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- if Mastodon::Feature.collections_enabled?
|
||||
|
||||
.fields-group
|
||||
= form.input :collection_limit,
|
||||
wrapper: :with_label
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- unless current_user.role == form.object
|
||||
|
||||
.field-group
|
||||
|
||||
@ -679,7 +679,7 @@ is:
|
||||
suspend_description_html: Notandaaðgangurinn og allt efni á honum mun verða óaðgengilegt og á endanum eytt út og samskipti við aðganginn verða ekki möguleg. Hægt að afturkalla innan 30 daga. Lokar öllum kærum gagnvart þessum aðgangi.
|
||||
actions_description_html: Ákveddu til hvaða aðgerða eigi að taka til að leysa þessa kæru. Ef þú ákveður að refsa kærða notandaaðgangnum, verður viðkomandi send tilkynning í tölvupósti, nema ef flokkurinn <strong>Ruslpóstur</strong> sé valinn.
|
||||
actions_description_remote_html: Ákveddu til hvaða aðgerða eigi að taka til að leysa þessa kæru. Þetta mun aðeins hafa áhrif á hvernig <strong>netþjónninn þinn</strong> meðhöndlar þennan fjartengda aðgang og efnið á honum.
|
||||
actions_no_posts: Þessi kæra er ekki með neinar tengdar færslur til að eyða
|
||||
actions_no_posts: Þessi tilkynning er ekki með neinar tengdar færslur til að eyða
|
||||
add_to_report: Bæta fleiru í kæru
|
||||
already_suspended_badges:
|
||||
local: Þegar frystur á þessum netþjóni
|
||||
@ -884,7 +884,7 @@ is:
|
||||
public: Allir
|
||||
landing_page:
|
||||
values:
|
||||
about: Um hugbúnaðinn
|
||||
about: Um
|
||||
local_feed: Staðbundið streymi
|
||||
trends: Vinsælt
|
||||
registrations:
|
||||
|
||||
@ -160,6 +160,7 @@ en:
|
||||
other: We have to make sure you're at least %{count} to use %{domain}. We won't store this.
|
||||
role: The role controls which permissions the user has.
|
||||
user_role:
|
||||
collection_limit: Limits the number of Collections that a single user with this role can create. Please note that when you decrease this number, users who are already at this limit will not lose any Collections. But they will not be able to create additional ones.
|
||||
color: Color to be used for the role throughout the UI, as RGB in hex format
|
||||
highlighted: This makes the role publicly visible
|
||||
name: Public name of the role, if role is set to be displayed as a badge
|
||||
@ -386,6 +387,7 @@ en:
|
||||
role: Role
|
||||
time_zone: Time zone
|
||||
user_role:
|
||||
collection_limit: Maximum number of Collections per user
|
||||
color: Badge color
|
||||
highlighted: Display role as badge on user profiles
|
||||
name: Name
|
||||
|
||||
@ -61,6 +61,9 @@ sv:
|
||||
setting_default_quote_policy_private: Inlägg som endast är för följare och som författats på Mastodon kan inte citeras av andra.
|
||||
setting_default_quote_policy_unlisted: När folk citerar dig, kommer deras inlägg också att döljas från trendande tidslinjer.
|
||||
setting_default_sensitive: Känslig media döljs som standard och kan visas med ett klick
|
||||
setting_display_media_default: Varna innan du visar media markerad som känslig
|
||||
setting_display_media_hide_all: Varna innan du visar alla medier
|
||||
setting_display_media_show_all: Visa alla medier utan varning, inklusive media markerad som känslig
|
||||
setting_emoji_style: Hur emojier visas. "Automatiskt" kommer att försöka använda webbläsarens emojier, men faller tillbaka till Twemoji för äldre webbläsare.
|
||||
setting_quick_boosting_html: När aktiverad, klicka på %{boost_icon} Boost-ikonen för att omedelbart boosta istället för att öppna boost/citera-rullgardinsmenyn. Flyttar citering till %{options_icon} (Alternativ)-menyn.
|
||||
setting_system_scrollbars_ui: Gäller endast för webbläsare som är baserade på Safari och Chrome
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddCollectionLimitToUserRoles < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :user_roles, :collection_limit, :integer, null: false, default: 10
|
||||
end
|
||||
end
|
||||
@ -7,7 +7,7 @@ class MigrateRoles < ActiveRecord::Migration[5.2]
|
||||
class User < ApplicationRecord; end
|
||||
|
||||
def up
|
||||
load Rails.root.join('db', 'seeds', '03_roles.rb')
|
||||
create_user_roles
|
||||
|
||||
owner_role = UserRole.find_by(name: 'Owner')
|
||||
moderator_role = UserRole.find_by(name: 'Moderator')
|
||||
@ -24,4 +24,30 @@ class MigrateRoles < ActiveRecord::Migration[5.2]
|
||||
User.where(role_id: [admin_role.id, owner_role.id]).in_batches.update_all(admin: true) if admin_role
|
||||
User.where(role_id: moderator_role.id).in_batches.update_all(moderator: true) if moderator_role
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_user_roles
|
||||
now = Time.zone.now.to_fs(:db)
|
||||
|
||||
safety_assured do
|
||||
execute <<~SQL.squish
|
||||
INSERT INTO user_roles ( id, permissions, created_at, updated_at )
|
||||
VALUES ( -99, 65536, '#{now}', '#{now}' )
|
||||
ON CONFLICT DO NOTHING
|
||||
SQL
|
||||
|
||||
[
|
||||
['Moderator', 10, 1308],
|
||||
['Admin', 100, 983_036],
|
||||
['Owner', 1000, 1],
|
||||
].each do |name, position, permissions|
|
||||
execute <<~SQL.squish
|
||||
INSERT INTO user_roles ( name, position, highlighted, permissions, created_at, updated_at )
|
||||
SELECT '#{name}', #{position}, true, #{permissions}, '#{now}', '#{now}'
|
||||
WHERE NOT EXISTS ( SELECT 1 FROM user_roles WHERE name = '#{name}' )
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_04_15_133505) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_04_20_124030) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
@ -1340,6 +1340,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_15_133505) do
|
||||
end
|
||||
|
||||
create_table "user_roles", force: :cascade do |t|
|
||||
t.integer "collection_limit", default: 10, null: false
|
||||
t.string "color", default: "", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.boolean "highlighted", default: false, null: false
|
||||
|
||||
@ -107,7 +107,6 @@
|
||||
"react-select": "^5.7.3",
|
||||
"react-sparklines": "^1.7.0",
|
||||
"react-textarea-autosize": "^8.4.1",
|
||||
"react-toggle": "^4.1.3",
|
||||
"redux-immutable": "^4.0.0",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"requestidlecallback": "^0.3.0",
|
||||
@ -160,7 +159,6 @@
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-sparklines": "^1.7.2",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"@types/react-toggle": "^4.0.3",
|
||||
"@types/redux-immutable": "^4.0.3",
|
||||
"@types/requestidlecallback": "^0.3.5",
|
||||
"@vitest/browser": "^4.1.0",
|
||||
|
||||
@ -71,6 +71,20 @@ RSpec.describe Collection do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is already at the per-user limit of collections' do
|
||||
subject { Fabricate.build(:collection, account:) }
|
||||
|
||||
let(:role) { Fabricate(:user_role, collection_limit: 2) }
|
||||
let(:user) { Fabricate(:user, role:) }
|
||||
let(:account) { user.account }
|
||||
|
||||
before do
|
||||
Fabricate.times(2, :collection, account:)
|
||||
end
|
||||
|
||||
it { is_expected.to_not be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#item_for' do
|
||||
|
||||
@ -31,6 +31,12 @@ RSpec.describe UserRole do
|
||||
it { is_expected.to_not allow_values('x', '112233445566', '#xxyyzz').for(:color) }
|
||||
end
|
||||
|
||||
describe 'collection_limit' do
|
||||
subject { Fabricate.build :user_role }
|
||||
|
||||
it { is_expected.to validate_numericality_of(:collection_limit).only_integer.is_greater_than_or_equal_to(0) }
|
||||
end
|
||||
|
||||
context 'when current_account is set' do
|
||||
subject { Fabricate :user_role }
|
||||
|
||||
|
||||
43
spec/serializers/rest/role_serializer_spec.rb
Normal file
43
spec/serializers/rest/role_serializer_spec.rb
Normal file
@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe REST::RoleSerializer do
|
||||
subject { serialized_record_json(role, described_class) }
|
||||
|
||||
let(:everyone) do
|
||||
Fabricate.build(:user_role, permissions: 0)
|
||||
end
|
||||
let(:role) do
|
||||
Fabricate.build(:user_role, id: 2342, name: 'test role', color: '#ABC', highlighted: true, permissions: 2300, collection_limit: 11)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(UserRole).to receive(:everyone).and_return(everyone)
|
||||
end
|
||||
|
||||
it 'includes the relevant attributes' do
|
||||
expect(subject)
|
||||
.to include({
|
||||
'id' => '2342',
|
||||
'name' => 'test role',
|
||||
'color' => '#ABC',
|
||||
'highlighted' => true,
|
||||
'permissions' => '2300',
|
||||
})
|
||||
end
|
||||
|
||||
context 'when collections are enabled', feature: :collections do
|
||||
it 'includes the relevant attributes' do
|
||||
expect(subject)
|
||||
.to include({
|
||||
'id' => '2342',
|
||||
'name' => 'test role',
|
||||
'color' => '#ABC',
|
||||
'highlighted' => true,
|
||||
'permissions' => '2300',
|
||||
'collection_limit' => 11,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -6,6 +6,7 @@ import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin';
|
||||
import babel from '@rolldown/plugin-babel';
|
||||
import legacy from '@vitejs/plugin-legacy';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import browserslist from 'browserslist';
|
||||
import postcssPresetEnv from 'postcss-preset-env';
|
||||
import Compress from 'rollup-plugin-gzip';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
@ -182,6 +183,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
|
||||
legacy({
|
||||
renderLegacyChunks: false,
|
||||
modernPolyfills: true,
|
||||
modernTargets: browserslist.loadConfig({ path: process.cwd() }),
|
||||
}),
|
||||
isProdBuild && (Compress() as PluginOption),
|
||||
command === 'build' &&
|
||||
|
||||
40
yarn.lock
40
yarn.lock
@ -2885,7 +2885,6 @@ __metadata:
|
||||
"@types/react-router-dom": "npm:^5.3.3"
|
||||
"@types/react-sparklines": "npm:^1.7.2"
|
||||
"@types/react-test-renderer": "npm:^18.0.0"
|
||||
"@types/react-toggle": "npm:^4.0.3"
|
||||
"@types/redux-immutable": "npm:^4.0.3"
|
||||
"@types/requestidlecallback": "npm:^0.3.5"
|
||||
"@use-gesture/react": "npm:^10.3.1"
|
||||
@ -2966,7 +2965,6 @@ __metadata:
|
||||
react-sparklines: "npm:^1.7.0"
|
||||
react-test-renderer: "npm:^18.2.0"
|
||||
react-textarea-autosize: "npm:^8.4.1"
|
||||
react-toggle: "npm:^4.1.3"
|
||||
redux-immutable: "npm:^4.0.0"
|
||||
regenerator-runtime: "npm:^0.14.0"
|
||||
requestidlecallback: "npm:^0.3.0"
|
||||
@ -4791,15 +4789,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-toggle@npm:^4.0.3":
|
||||
version: 4.0.5
|
||||
resolution: "@types/react-toggle@npm:4.0.5"
|
||||
dependencies:
|
||||
"@types/react": "npm:*"
|
||||
checksum: 10c0/f557b85c96715b145bcc3beb2903f88ee3a6045ef85da0f80561c7cc2ecdc531e2d4ae121ed8ec3a1761264de25b8410653744093f37abf042201587add7ffa6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-transition-group@npm:^4.4.0":
|
||||
version: 4.4.8
|
||||
resolution: "@types/react-transition-group@npm:4.4.8"
|
||||
@ -5927,11 +5916,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"baseline-browser-mapping@npm:^2.9.0":
|
||||
version: 2.9.2
|
||||
resolution: "baseline-browser-mapping@npm:2.9.2"
|
||||
version: 2.10.20
|
||||
resolution: "baseline-browser-mapping@npm:2.10.20"
|
||||
bin:
|
||||
baseline-browser-mapping: dist/cli.js
|
||||
checksum: 10c0/4f9be09e20261ed26f19e9b95454dcb8d8371b87983c57cd9f70b9572e9b3053577f0d8d6d91297bdb605337747680686e22f62522a6e57ae2488fcacf641188
|
||||
baseline-browser-mapping: dist/cli.cjs
|
||||
checksum: 10c0/3d60c9656c4c4673593aa8d0ae9aa6b69b4e018c2f585874a0e8a40cb28d0559f57ee1b2e7e44cb1e7f6aac66f658a4a3c1285901b8836d8ae31e189e30aa816
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -6160,9 +6149,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"caniuse-lite@npm:^1.0.30001759, caniuse-lite@npm:^1.0.30001766":
|
||||
version: 1.0.30001770
|
||||
resolution: "caniuse-lite@npm:1.0.30001770"
|
||||
checksum: 10c0/02d15a8b723af65318cb4d888a52bb090076898da7b0de99e8676d537f8d1d2ae4797e81518e1e30cbfe84c33b048c322e8bfafc5b23cfee8defb0d2bf271149
|
||||
version: 1.0.30001790
|
||||
resolution: "caniuse-lite@npm:1.0.30001790"
|
||||
checksum: 10c0/eec0adc1dcb35d51e57bcfa0657493cb57ef43f0ceb03c1edcfee34d43e7a938e6beed2781118c7a5ee99d4f71d443977f08ca5a549005cf89260733af9ad3f8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -6245,7 +6234,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"classnames@npm:^2.2.5, classnames@npm:^2.3.2":
|
||||
"classnames@npm:^2.3.2":
|
||||
version: 2.5.1
|
||||
resolution: "classnames@npm:2.5.1"
|
||||
checksum: 10c0/afff4f77e62cea2d79c39962980bf316bacb0d7c49e13a21adaadb9221e1c6b9d3cdb829d8bb1b23c406f4e740507f37e1dcf506f7e3b7113d17c5bab787aa69
|
||||
@ -12290,19 +12279,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-toggle@npm:^4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "react-toggle@npm:4.1.3"
|
||||
dependencies:
|
||||
classnames: "npm:^2.2.5"
|
||||
peerDependencies:
|
||||
prop-types: ">= 15.3.0 < 19"
|
||||
react: ">= 15.3.0 < 19"
|
||||
react-dom: ">= 15.3.0 < 19"
|
||||
checksum: 10c0/afae643981d6fb922d66830ec0e9c793e729d7a55e3974edf212965acc8dc51f0d098ae6e409461d3ff2f87fe027dc3556a1a746f8472ed3a79c8878bf338fee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-transition-group@npm:^4.3.0":
|
||||
version: 4.4.5
|
||||
resolution: "react-transition-group@npm:4.4.5"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user