Merge remote-tracking branch 'origin/develop' into theme-editor

environments/review-theme-edit-1forjd/deployments/1789
Alex Gleason 2022-12-17 17:57:43 -06:00
commit 8a14dfefe7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
18 zmienionych plików z 340 dodań i 266 usunięć

Wyświetl plik

@ -25,6 +25,7 @@ deps:
cache: cache:
<<: *cache <<: *cache
policy: push policy: push
interruptible: true
danger: danger:
stage: test stage: test
@ -33,6 +34,7 @@ danger:
- export CI_MERGE_REQUEST_IID=${CI_OPEN_MERGE_REQUESTS#*!} - export CI_MERGE_REQUEST_IID=${CI_OPEN_MERGE_REQUESTS#*!}
- npx danger ci - npx danger ci
allow_failure: true allow_failure: true
interruptible: true
lint-js: lint-js:
stage: test stage: test
@ -45,6 +47,7 @@ lint-js:
- "**/*.tsx" - "**/*.tsx"
- ".eslintignore" - ".eslintignore"
- ".eslintrc.js" - ".eslintrc.js"
interruptible: true
lint-sass: lint-sass:
stage: test stage: test
@ -54,6 +57,7 @@ lint-sass:
- "**/*.scss" - "**/*.scss"
- "**/*.css" - "**/*.css"
- ".stylelintrc.json" - ".stylelintrc.json"
interruptible: true
jest: jest:
stage: test stage: test
@ -76,6 +80,7 @@ jest:
coverage_report: coverage_report:
coverage_format: cobertura coverage_format: cobertura
path: .coverage/cobertura-coverage.xml path: .coverage/cobertura-coverage.xml
interruptible: true
nginx-test: nginx-test:
stage: test stage: test
@ -85,6 +90,7 @@ nginx-test:
only: only:
changes: changes:
- "installation/mastodon.conf" - "installation/mastodon.conf"
interruptible: true
build-production: build-production:
stage: test stage: test
@ -94,6 +100,7 @@ build-production:
artifacts: artifacts:
paths: paths:
- static - static
interruptible: true
docs-deploy: docs-deploy:
stage: deploy stage: deploy
@ -107,6 +114,7 @@ docs-deploy:
- develop - develop
changes: changes:
- "docs/**/*" - "docs/**/*"
interruptible: true
# Supposed to fail when translations are outdated, instead always passes # Supposed to fail when translations are outdated, instead always passes
# #
@ -127,6 +135,7 @@ review:
script: script:
- npx -y surge static $CI_COMMIT_REF_SLUG.git.soapbox.pub - npx -y surge static $CI_COMMIT_REF_SLUG.git.soapbox.pub
allow_failure: true allow_failure: true
interruptible: true
pages: pages:
stage: deploy stage: deploy
@ -142,6 +151,7 @@ pages:
only: only:
refs: refs:
- develop - develop
interruptible: true
docker: docker:
stage: deploy stage: deploy
@ -157,4 +167,5 @@ docker:
- docker push $CI_REGISTRY_IMAGE - docker push $CI_REGISTRY_IMAGE
only: only:
refs: refs:
- develop - develop
interruptible: true

Wyświetl plik

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Events: ability to create, view, and comment on Events (on Rebased). - Events: ability to create, view, and comment on Events (on Rebased).
- Onboarding: display an introduction wizard to newly registered accounts. - Onboarding: display an introduction wizard to newly registered accounts.
- Posts: translate foreign language posts into your native language (on Rebased, Mastodon; if configured by the admin). - Posts: translate foreign language posts into your native language (on Rebased, Mastodon; if configured by the admin).
- Posts: ability to view quotes of a post (on Rebased).
- Posts: hover the "replying to" line to see a preview card of the parent post. - Posts: hover the "replying to" line to see a preview card of the parent post.
- Chats: ability to leave a chat (on Rebased, Truth Social). - Chats: ability to leave a chat (on Rebased, Truth Social).
- Chats: ability to disable chats for yourself. - Chats: ability to disable chats for yourself.
@ -32,10 +33,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Posts: changed the thumbs-up icon to a heart. - Posts: changed the thumbs-up icon to a heart.
- Posts: move instance favicon beside username instead of post timestamp. - Posts: move instance favicon beside username instead of post timestamp.
- Posts: changed the behavior of content warnings. CWs and sensitive media are unified into one design. - Posts: changed the behavior of content warnings. CWs and sensitive media are unified into one design.
- Posts: redesigned interaction counters to use text instead of icons.
- Profile: overhauled user profiles to be consistent with the rest of the UI. - Profile: overhauled user profiles to be consistent with the rest of the UI.
- Composer: move emoji button alongside other composer buttons, add numerical counter. - Composer: move emoji button alongside other composer buttons, add numerical counter.
- Birthdays: move today's birthdays out of notifications into right sidebar. - Birthdays: move today's birthdays out of notifications into right sidebar.
- Performance: improve scrolling/navigation between feeds by using a virtual window library. - Performance: improve scrolling/navigation between feeds by using a virtual window library.
- Admin: reorganize UI into 3-column layout.
- Admin: include external link to frontend repo for the running commit.
### Removed ### Removed
- Theme: Halloween theme. - Theme: Halloween theme.

Wyświetl plik

@ -28,7 +28,7 @@ busybox unzip soapbox.zip -o -d /opt/pleroma/instance
The change will take effect immediately, just refresh your browser tab. The change will take effect immediately, just refresh your browser tab.
It's not necessary to restart the Pleroma service. It's not necessary to restart the Pleroma service.
***For OTP releases,*** *unpack to /var/lib/pleroma instead.* **_For OTP releases,_** _unpack to /var/lib/pleroma instead._
To remove Soapbox and revert to the default pleroma-fe, simply `rm /opt/pleroma/instance/static/index.html` (you can delete other stuff in there too, but be careful not to delete your own HTML files). To remove Soapbox and revert to the default pleroma-fe, simply `rm /opt/pleroma/instance/static/index.html` (you can delete other stuff in there too, but be careful not to delete your own HTML files).
@ -150,15 +150,19 @@ NODE_ENV=development
``` ```
#### Local dev server #### Local dev server
- `yarn dev` - Run the local dev server. - `yarn dev` - Run the local dev server.
#### Building #### Building
- `yarn build` - Compile without a dev server, into `/static` directory. - `yarn build` - Compile without a dev server, into `/static` directory.
#### Translations #### Translations
- `yarn manage:translations` - Normalizes translation files. Should always be run after editing i18n strings. - `yarn manage:translations` - Normalizes translation files. Should always be run after editing i18n strings.
#### Tests #### Tests
- `yarn test:all` - Runs all tests and linters. - `yarn test:all` - Runs all tests and linters.
- `yarn test` - Runs Jest for frontend unit tests. - `yarn test` - Runs Jest for frontend unit tests.
@ -174,6 +178,9 @@ NODE_ENV=development
We welcome contributions to this project. We welcome contributions to this project.
To contribute, see [Contributing to Soapbox](docs/contributing.md). To contribute, see [Contributing to Soapbox](docs/contributing.md).
Translators can help by providing [translations through Weblate](http://hosted.weblate.org/soapbox-pub/soapbox/).
Native speakers from all around the world are welcome!
# Customization # Customization
Soapbox supports customization of the user interface, to allow per-instance branding and other features. Soapbox supports customization of the user interface, to allow per-instance branding and other features.
@ -205,8 +212,8 @@ the Free Software Foundation, either version 3 of the License, or
Soapbox is distributed in the hope that it will be useful, Soapbox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with Soapbox. If not, see <https://www.gnu.org/licenses/>. along with Soapbox. If not, see <https://www.gnu.org/licenses/>.

Wyświetl plik

@ -0,0 +1,43 @@
import React from 'react';
import List, { ListItem } from './list';
interface IRadioGroup {
onChange: React.ChangeEventHandler
children: React.ReactElement<{ onChange: React.ChangeEventHandler }>[]
}
const RadioGroup = ({ onChange, children }: IRadioGroup) => {
const childrenWithProps = React.Children.map(children, child =>
React.cloneElement(child, { onChange }),
);
return <List>{childrenWithProps}</List>;
};
interface IRadioItem {
label: React.ReactNode,
hint?: React.ReactNode,
value: string,
checked: boolean,
onChange?: React.ChangeEventHandler,
}
const RadioItem: React.FC<IRadioItem> = ({ label, hint, checked = false, onChange, value }) => {
return (
<ListItem label={label} hint={hint}>
<input
type='radio'
checked={checked}
onChange={onChange}
value={value}
className='h-4 w-4 border-gray-300 text-primary-600 focus:ring-primary-500'
/>
</ListItem>
);
};
export {
RadioGroup,
RadioItem,
};

Wyświetl plik

@ -0,0 +1,57 @@
import React from 'react';
import { FormattedNumber } from 'react-intl';
import { Link } from 'react-router-dom';
import { Text } from 'soapbox/components/ui';
import { isNumber } from 'soapbox/utils/numbers';
interface IDashCounter {
count: number | undefined
label: React.ReactNode
to?: string
percent?: boolean
}
/** Displays a (potentially clickable) dashboard statistic. */
const DashCounter: React.FC<IDashCounter> = ({ count, label, to = '#', percent = false }) => {
if (!isNumber(count)) {
return null;
}
return (
<Link
className='bg-gray-200 dark:bg-gray-800 p-4 rounded flex flex-col items-center space-y-2 hover:-translate-y-1 transition-transform cursor-pointer'
to={to}
>
<Text align='center' size='2xl' weight='medium'>
<FormattedNumber
value={count}
style={percent ? 'unit' : undefined}
unit={percent ? 'percent' : undefined}
/>
</Text>
<Text align='center'>
{label}
</Text>
</Link>
);
};
interface IDashCounters {
children: React.ReactNode
}
/** Wrapper container for dash counters. */
const DashCounters: React.FC<IDashCounters> = ({ children }) => {
return (
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2'>
{children}
</div>
);
};
export {
DashCounter,
DashCounters,
};

Wyświetl plik

@ -3,12 +3,7 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { updateConfig } from 'soapbox/actions/admin'; import { updateConfig } from 'soapbox/actions/admin';
import snackbar from 'soapbox/actions/snackbar'; import snackbar from 'soapbox/actions/snackbar';
import { import { RadioGroup, RadioItem } from 'soapbox/components/radio';
SimpleForm,
FieldsGroup,
RadioGroup,
RadioItem,
} from 'soapbox/features/forms';
import { useAppDispatch, useInstance } from 'soapbox/hooks'; import { useAppDispatch, useInstance } from 'soapbox/hooks';
import type { Instance } from 'soapbox/types/entities'; import type { Instance } from 'soapbox/types/entities';
@ -54,33 +49,26 @@ const RegistrationModePicker: React.FC = () => {
}; };
return ( return (
<SimpleForm> <RadioGroup onChange={onChange}>
<FieldsGroup> <RadioItem
<RadioGroup label={<FormattedMessage id='admin.dashboard.registration_mode.open_label' defaultMessage='Open' />}
label={<FormattedMessage id='admin.dashboard.registration_mode_label' defaultMessage='Registrations' />} hint={<FormattedMessage id='admin.dashboard.registration_mode.open_hint' defaultMessage='Anyone can join.' />}
onChange={onChange} checked={mode === 'open'}
> value='open'
<RadioItem />
label={<FormattedMessage id='admin.dashboard.registration_mode.open_label' defaultMessage='Open' />} <RadioItem
hint={<FormattedMessage id='admin.dashboard.registration_mode.open_hint' defaultMessage='Anyone can join.' />} label={<FormattedMessage id='admin.dashboard.registration_mode.approval_label' defaultMessage='Approval Required' />}
checked={mode === 'open'} hint={<FormattedMessage id='admin.dashboard.registration_mode.approval_hint' defaultMessage='Users can sign up, but their account only gets activated when an admin approves it.' />}
value='open' checked={mode === 'approval'}
/> value='approval'
<RadioItem />
label={<FormattedMessage id='admin.dashboard.registration_mode.approval_label' defaultMessage='Approval Required' />} <RadioItem
hint={<FormattedMessage id='admin.dashboard.registration_mode.approval_hint' defaultMessage='Users can sign up, but their account only gets activated when an admin approves it.' />} label={<FormattedMessage id='admin.dashboard.registration_mode.closed_label' defaultMessage='Closed' />}
checked={mode === 'approval'} hint={<FormattedMessage id='admin.dashboard.registration_mode.closed_hint' defaultMessage='Nobody can sign up. You can still invite people.' />}
value='approval' checked={mode === 'closed'}
/> value='closed'
<RadioItem />
label={<FormattedMessage id='admin.dashboard.registration_mode.closed_label' defaultMessage='Closed' />} </RadioGroup>
hint={<FormattedMessage id='admin.dashboard.registration_mode.closed_hint' defaultMessage='Nobody can sign up. You can still invite people.' />}
checked={mode === 'closed'}
value='closed'
/>
</RadioGroup>
</FieldsGroup>
</SimpleForm>
); );
}; };

Wyświetl plik

@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { approveUsers } from 'soapbox/actions/admin'; import { approveUsers } from 'soapbox/actions/admin';
import { rejectUserModal } from 'soapbox/actions/moderation'; import { rejectUserModal } from 'soapbox/actions/moderation';
import snackbar from 'soapbox/actions/snackbar'; import snackbar from 'soapbox/actions/snackbar';
import IconButton from 'soapbox/components/icon-button'; import { Stack, HStack, Text, IconButton } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors'; import { makeGetAccount } from 'soapbox/selectors';
@ -45,16 +45,31 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
}; };
return ( return (
<div className='unapproved-account'> <HStack space={4} justifyContent='between'>
<div className='unapproved-account__bio'> <Stack space={1}>
<div className='unapproved-account__nickname'>@{account.get('acct')}</div> <Text weight='semibold'>
<blockquote className='md'>{adminAccount?.invite_request || ''}</blockquote> @{account.get('acct')}
</div> </Text>
<div className='unapproved-account__actions'> <Text tag='blockquote' size='sm'>
<IconButton src={require('@tabler/icons/check.svg')} onClick={handleApprove} /> {adminAccount?.invite_request || ''}
<IconButton src={require('@tabler/icons/x.svg')} onClick={handleReject} /> </Text>
</div> </Stack>
</div>
<HStack space={2} alignItems='center'>
<IconButton
src={require('@tabler/icons/check.svg')}
onClick={handleApprove}
theme='outlined'
iconClassName='p-1 text-gray-600 dark:text-gray-400'
/>
<IconButton
src={require('@tabler/icons/x.svg')}
onClick={handleReject}
theme='outlined'
iconClassName='p-1 text-gray-600 dark:text-gray-400'
/>
</HStack>
</HStack>
); );
}; };

Wyświetl plik

@ -3,8 +3,9 @@ import { defineMessages, FormattedDate, useIntl } from 'react-intl';
import { fetchModerationLog } from 'soapbox/actions/admin'; import { fetchModerationLog } from 'soapbox/actions/admin';
import ScrollableList from 'soapbox/components/scrollable-list'; import ScrollableList from 'soapbox/components/scrollable-list';
import { Column } from 'soapbox/components/ui'; import { Column, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { AdminLog } from 'soapbox/types/entities';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.admin.moderation_log', defaultMessage: 'Moderation Log' }, heading: { id: 'column.admin.moderation_log', defaultMessage: 'Moderation Log' },
@ -18,6 +19,7 @@ const ModerationLog = () => {
const items = useAppSelector((state) => { const items = useAppSelector((state) => {
return state.admin_log.index.map((i) => state.admin_log.items.get(String(i))); return state.admin_log.index.map((i) => state.admin_log.items.get(String(i)));
}); });
const hasMore = useAppSelector((state) => state.admin_log.total - state.admin_log.index.count() > 0); const hasMore = useAppSelector((state) => state.admin_log.total - state.admin_log.index.count() > 0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -54,26 +56,38 @@ const ModerationLog = () => {
emptyMessage={intl.formatMessage(messages.emptyMessage)} emptyMessage={intl.formatMessage(messages.emptyMessage)}
hasMore={hasMore} hasMore={hasMore}
onLoadMore={handleLoadMore} onLoadMore={handleLoadMore}
className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'
> >
{items.map((item) => item && ( {items.map(item => item && (
<div className='logentry' key={item.id}> <LogItem key={item.id} log={item} />
<div className='logentry__message'>{item.message}</div>
<div className='logentry__timestamp'>
<FormattedDate
value={new Date(item.time * 1000)}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
</div>
</div>
))} ))}
</ScrollableList> </ScrollableList>
</Column> </Column>
); );
}; };
interface ILogItem {
log: AdminLog
}
const LogItem: React.FC<ILogItem> = ({ log }) => {
return (
<Stack space={2} className='p-4'>
<Text>{log.message}</Text>
<Text theme='muted' size='xs'>
<FormattedDate
value={new Date(log.time * 1000)}
hour12
year='numeric'
month='short'
day='2-digit'
hour='numeric'
minute='2-digit'
/>
</Text>
</Stack>
);
};
export default ModerationLog; export default ModerationLog;

Wyświetl plik

@ -33,9 +33,12 @@ const AwaitingApproval: React.FC = () => {
showLoading={showLoading} showLoading={showLoading}
scrollKey='awaiting-approval' scrollKey='awaiting-approval'
emptyMessage={intl.formatMessage(messages.emptyMessage)} emptyMessage={intl.formatMessage(messages.emptyMessage)}
className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'
> >
{accountIds.map(id => ( {accountIds.map(id => (
<UnapprovedAccount accountId={id} key={id} /> <div key={id} className='py-4 px-5'>
<UnapprovedAccount accountId={id} />
</div>
))} ))}
</ScrollableList> </ScrollableList>
); );

Wyświetl plik

@ -1,19 +1,21 @@
import React from 'react'; import React from 'react';
import { FormattedMessage, FormattedNumber } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list'; import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list';
import { Text } from 'soapbox/components/ui'; import List, { ListItem } from 'soapbox/components/list';
import { CardTitle, Icon, IconButton, Stack } from 'soapbox/components/ui';
import { useAppDispatch, useOwnAccount, useFeatures, useInstance } from 'soapbox/hooks'; import { useAppDispatch, useOwnAccount, useFeatures, useInstance } from 'soapbox/hooks';
import sourceCode from 'soapbox/utils/code'; import sourceCode from 'soapbox/utils/code';
import { download } from 'soapbox/utils/download'; import { download } from 'soapbox/utils/download';
import { parseVersion } from 'soapbox/utils/features'; import { parseVersion } from 'soapbox/utils/features';
import { isNumber } from 'soapbox/utils/numbers';
import { DashCounter, DashCounters } from '../components/dashcounter';
import RegistrationModePicker from '../components/registration-mode-picker'; import RegistrationModePicker from '../components/registration-mode-picker';
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const history = useHistory();
const instance = useInstance(); const instance = useInstance();
const features = useFeatures(); const features = useFeatures();
const account = useOwnAccount(); const account = useOwnAccount();
@ -39,6 +41,9 @@ const Dashboard: React.FC = () => {
e.preventDefault(); e.preventDefault();
}; };
const navigateToSoapboxConfig = () => history.push('/soapbox/config');
const navigateToModerationLog = () => history.push('/soapbox/admin/log');
const v = parseVersion(instance.version); const v = parseVersion(instance.version);
const userCount = instance.stats.get('user_count'); const userCount = instance.stats.get('user_count');
@ -46,87 +51,121 @@ const Dashboard: React.FC = () => {
const domainCount = instance.stats.get('domain_count'); const domainCount = instance.stats.get('domain_count');
const mau = instance.pleroma.getIn(['stats', 'mau']) as number | undefined; const mau = instance.pleroma.getIn(['stats', 'mau']) as number | undefined;
const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : null; const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : undefined;
if (!account) return null; if (!account) return null;
return ( return (
<> <Stack space={6} className='mt-4'>
<div className='dashcounters mt-8'> <DashCounters>
{isNumber(mau) && ( <DashCounter
<div className='dashcounter'> count={mau}
<Text align='center' size='2xl' weight='medium'> label={<FormattedMessage id='admin.dashcounters.mau_label' defaultMessage='monthly active users' />}
<FormattedNumber value={mau} /> />
</Text> <DashCounter
<Text align='center'> to='/soapbox/admin/users'
<FormattedMessage id='admin.dashcounters.mau_label' defaultMessage='monthly active users' /> count={userCount}
</Text> label={<FormattedMessage id='admin.dashcounters.user_count_label' defaultMessage='total users' />}
</div> />
)} <DashCounter
{isNumber(userCount) && ( count={retention}
<Link className='dashcounter' to='/soapbox/admin/users'> label={<FormattedMessage id='admin.dashcounters.retention_label' defaultMessage='user retention' />}
<Text align='center' size='2xl' weight='medium'> percent
<FormattedNumber value={userCount} /> />
</Text> <DashCounter
<Text align='center'> to='/timeline/local'
<FormattedMessage id='admin.dashcounters.user_count_label' defaultMessage='total users' /> count={statusCount}
</Text> label={<FormattedMessage id='admin.dashcounters.status_count_label' defaultMessage='posts' />}
</Link> />
)} <DashCounter
{isNumber(retention) && ( count={domainCount}
<div className='dashcounter'> label={<FormattedMessage id='admin.dashcounters.domain_count_label' defaultMessage='peers' />}
<Text align='center' size='2xl' weight='medium'> />
{retention}% </DashCounters>
</Text>
<Text align='center'>
<FormattedMessage id='admin.dashcounters.retention_label' defaultMessage='user retention' />
</Text>
</div>
)}
{isNumber(statusCount) && (
<Link className='dashcounter' to='/timeline/local'>
<Text align='center' size='2xl' weight='medium'>
<FormattedNumber value={statusCount} />
</Text>
<Text align='center'>
<FormattedMessage id='admin.dashcounters.status_count_label' defaultMessage='posts' />
</Text>
</Link>
)}
{isNumber(domainCount) && (
<div className='dashcounter'>
<Text align='center' size='2xl' weight='medium'>
<FormattedNumber value={domainCount} />
</Text>
<Text align='center'>
<FormattedMessage id='admin.dashcounters.domain_count_label' defaultMessage='peers' />
</Text>
</div>
)}
</div>
{account.admin && <RegistrationModePicker />} <List>
{account.admin && (
<div className='dashwidgets'> <ListItem
<div className='dashwidget'> onClick={navigateToSoapboxConfig}
<h4><FormattedMessage id='admin.dashwidgets.software_header' defaultMessage='Software' /></h4> label={<FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' />}
<ul> />
<li>{sourceCode.displayName} <span className='pull-right'>{sourceCode.version}</span></li>
<li>{v.software + (v.build ? `+${v.build}` : '')} <span className='pull-right'>{v.version}</span></li>
</ul>
</div>
{features.emailList && account.admin && (
<div className='dashwidget'>
<h4><FormattedMessage id='admin.dashwidgets.email_list_header' defaultMessage='Email list' /></h4>
<ul>
<li><a href='#' onClick={handleSubscribersClick} target='_blank'>subscribers.csv</a></li>
<li><a href='#' onClick={handleUnsubscribersClick} target='_blank'>unsubscribers.csv</a></li>
<li><a href='#' onClick={handleCombinedClick} target='_blank'>combined.csv</a></li>
</ul>
</div>
)} )}
</div>
</> <ListItem
onClick={navigateToModerationLog}
label={<FormattedMessage id='column.admin.moderation_log' defaultMessage='Moderation Log' />}
/>
</List>
{account.admin && (
<>
<CardTitle
title={<FormattedMessage id='admin.dashboard.registration_mode_label' defaultMessage='Registrations' />}
/>
<RegistrationModePicker />
</>
)}
<CardTitle
title={<FormattedMessage id='admin.dashwidgets.software_header' defaultMessage='Software' />}
/>
<List>
<ListItem label={<FormattedMessage id='admin.software.frontend' defaultMessage='Frontend' />}>
<a
href={sourceCode.ref ? `${sourceCode.url}/tree/${sourceCode.ref}` : sourceCode.url}
className='flex space-x-1 items-center truncate'
target='_blank'
>
<span>{sourceCode.displayName} {sourceCode.version}</span>
<Icon
className='w-4 h-4'
src={require('@tabler/icons/external-link.svg')}
/>
</a>
</ListItem>
<ListItem label={<FormattedMessage id='admin.software.backend' defaultMessage='Backend' />}>
<span>{v.software + (v.build ? `+${v.build}` : '')} {v.version}</span>
</ListItem>
</List>
{(features.emailList && account.admin) && (
<>
<CardTitle
title={<FormattedMessage id='admin.dashwidgets.email_list_header' defaultMessage='Email list' />}
/>
<List>
<ListItem label='subscribers.csv'>
<IconButton
src={require('@tabler/icons/download.svg')}
onClick={handleSubscribersClick}
iconClassName='w-5 h-5'
/>
</ListItem>
<ListItem label='unsubscribers.csv'>
<IconButton
src={require('@tabler/icons/download.svg')}
onClick={handleUnsubscribersClick}
iconClassName='w-5 h-5'
/>
</ListItem>
<ListItem label='combined.csv'>
<IconButton
src={require('@tabler/icons/download.svg')}
onClick={handleCombinedClick}
iconClassName='w-5 h-5'
/>
</ListItem>
</List>
</>
)}
</Stack>
); );
}; };

Wyświetl plik

@ -1,8 +1,8 @@
import classNames from 'clsx'; import classNames from 'clsx';
import React, { useState, useRef } from 'react'; import React, { useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Text, Select } from '../../components/ui'; import { Select } from '../../components/ui';
interface IInputContainer { interface IInputContainer {
label?: React.ReactNode, label?: React.ReactNode,
@ -175,52 +175,6 @@ export const Checkbox: React.FC<ICheckbox> = (props) => (
<SimpleInput type='checkbox' {...props} /> <SimpleInput type='checkbox' {...props} />
); );
interface IRadioGroup {
label?: React.ReactNode,
onChange?: React.ChangeEventHandler,
}
export const RadioGroup: React.FC<IRadioGroup> = (props) => {
const { label, children, onChange } = props;
const childrenWithProps = React.Children.map(children, child =>
// @ts-ignore
React.cloneElement(child, { onChange }),
);
return (
<div className='input with_floating_label radio_buttons'>
<div className='label_input'>
<label>{label}</label>
<ul>{childrenWithProps}</ul>
</div>
</div>
);
};
interface IRadioItem {
label?: React.ReactNode,
hint?: React.ReactNode,
value: string,
checked: boolean,
onChange?: React.ChangeEventHandler,
}
export const RadioItem: React.FC<IRadioItem> = (props) => {
const { current: id } = useRef<string>(uuidv4());
const { label, hint, checked = false, ...rest } = props;
return (
<li className='radio'>
<label htmlFor={id}>
<input id={id} type='radio' checked={checked} {...rest} />
<Text>{label}</Text>
{hint && <span className='hint'>{hint}</span>}
</label>
</li>
);
};
interface ISelectDropdown { interface ISelectDropdown {
label?: React.ReactNode, label?: React.ReactNode,
hint?: React.ReactNode, hint?: React.ReactNode,

Wyświetl plik

@ -45,7 +45,7 @@ class Bundle extends React.PureComponent<BundleProps, BundleState> {
this.load(this.props); this.load(this.props);
} }
componentWillReceiveProps(nextProps: BundleProps) { UNSAFE_componentWillReceiveProps(nextProps: BundleProps) {
if (nextProps.fetchComponent !== this.props.fetchComponent) { if (nextProps.fetchComponent !== this.props.fetchComponent) {
this.load(nextProps); this.load(nextProps);
} }

Wyświetl plik

@ -8,7 +8,7 @@ import { ADMIN_LOG_FETCH_SUCCESS } from 'soapbox/actions/admin';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
const LogEntryRecord = ImmutableRecord({ export const LogEntryRecord = ImmutableRecord({
data: ImmutableMap<string, any>(), data: ImmutableMap<string, any>(),
id: 0, id: 0,
message: '', message: '',

Wyświetl plik

@ -24,10 +24,12 @@ import {
StatusRecord, StatusRecord,
TagRecord, TagRecord,
} from 'soapbox/normalizers'; } from 'soapbox/normalizers';
import { LogEntryRecord } from 'soapbox/reducers/admin-log';
import type { Record as ImmutableRecord } from 'immutable'; import type { Record as ImmutableRecord } from 'immutable';
type AdminAccount = ReturnType<typeof AdminAccountRecord>; type AdminAccount = ReturnType<typeof AdminAccountRecord>;
type AdminLog = ReturnType<typeof LogEntryRecord>;
type AdminReport = ReturnType<typeof AdminReportRecord>; type AdminReport = ReturnType<typeof AdminReportRecord>;
type Announcement = ReturnType<typeof AnnouncementRecord>; type Announcement = ReturnType<typeof AnnouncementRecord>;
type AnnouncementReaction = ReturnType<typeof AnnouncementReactionRecord>; type AnnouncementReaction = ReturnType<typeof AnnouncementReactionRecord>;
@ -68,6 +70,7 @@ type EmbeddedEntity<T extends object> = null | string | ReturnType<ImmutableReco
export { export {
AdminAccount, AdminAccount,
AdminLog,
AdminReport, AdminReport,
Account, Account,
Announcement, Announcement,

Wyświetl plik

@ -3,6 +3,8 @@ const { execSync } = require('child_process');
const pkg = require('../../../package.json'); const pkg = require('../../../package.json');
const { CI_COMMIT_TAG, CI_COMMIT_REF_NAME, CI_COMMIT_SHA } = process.env;
const shortRepoName = url => new URL(url).pathname.substring(1); const shortRepoName = url => new URL(url).pathname.substring(1);
const trimHash = hash => hash.substring(0, 7); const trimHash = hash => hash.substring(0, 7);
@ -10,14 +12,12 @@ const tryGit = cmd => {
try { try {
return String(execSync(cmd)); return String(execSync(cmd));
} catch (e) { } catch (e) {
return null; return undefined;
} }
}; };
const version = pkg => { const version = pkg => {
// Try to discern from GitLab CI first // Try to discern from GitLab CI first
const { CI_COMMIT_TAG, CI_COMMIT_REF_NAME, CI_COMMIT_SHA } = process.env;
if (CI_COMMIT_TAG === `v${pkg.version}` || CI_COMMIT_REF_NAME === 'stable') { if (CI_COMMIT_TAG === `v${pkg.version}` || CI_COMMIT_REF_NAME === 'stable') {
return pkg.version; return pkg.version;
} }
@ -43,4 +43,5 @@ module.exports = {
repository: shortRepoName(pkg.repository.url), repository: shortRepoName(pkg.repository.url),
version: version(pkg), version: version(pkg),
homepage: pkg.homepage, homepage: pkg.homepage,
ref: CI_COMMIT_TAG || CI_COMMIT_SHA || tryGit('git rev-parse HEAD'),
}; };

Wyświetl plik

@ -50,7 +50,6 @@
@import 'components/profile-hover-card'; @import 'components/profile-hover-card';
@import 'components/filters'; @import 'components/filters';
@import 'components/snackbar'; @import 'components/snackbar';
@import 'components/admin';
@import 'components/backups'; @import 'components/backups';
@import 'components/crypto-donate'; @import 'components/crypto-donate';
@import 'components/aliases'; @import 'components/aliases';

Wyświetl plik

@ -1,67 +0,0 @@
.dashcounters {
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 mb-4;
}
.dashcounter {
@apply bg-gray-200 dark:bg-gray-800 p-4 rounded flex flex-col items-center space-y-2 hover:-translate-y-1 transition-transform cursor-pointer;
}
.dashwidgets {
display: flex;
flex-wrap: wrap;
margin: 0 -5px;
padding: 0 20px 20px 20px;
}
.dashwidget {
flex: 1;
margin-bottom: 20px;
padding: 0 5px;
h4 {
text-transform: uppercase;
font-size: 13px;
font-weight: 700;
color: hsla(var(--primary-text-color_hsl), 0.6);
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid var(--accent-color--med);
}
a {
color: var(--brand-color);
}
}
.unapproved-account {
padding: 15px 20px;
font-size: 14px;
display: flex;
&__nickname {
font-weight: bold;
}
&__actions {
margin-left: auto;
display: flex;
flex-wrap: nowrap;
column-gap: 10px;
padding-left: 20px;
.svg-icon {
height: 24px;
width: 24px;
}
}
}
.logentry {
padding: 15px;
&__timestamp {
color: var(--primary-text-color--faint);
font-size: 13px;
text-align: right;
}
}

Wyświetl plik

@ -1,5 +1,8 @@
import { danger, warn, message } from 'danger'; import { danger, warn, message } from 'danger';
// App changes
const app = danger.git.fileMatch('app/soapbox/**');
// Docs changes // Docs changes
const docs = danger.git.fileMatch('docs/**/*.md'); const docs = danger.git.fileMatch('docs/**/*.md');
@ -10,7 +13,7 @@ if (docs.edited) {
// Enforce CHANGELOG.md additions // Enforce CHANGELOG.md additions
const changelog = danger.git.fileMatch('CHANGELOG.md'); const changelog = danger.git.fileMatch('CHANGELOG.md');
if (!changelog.edited) { if (app.edited && !changelog.edited) {
warn('You have not updated `CHANGELOG.md`. If this change directly impacts admins or users, please update the changelog. Otherwise you can ignore this message. See: https://keepachangelog.com'); warn('You have not updated `CHANGELOG.md`. If this change directly impacts admins or users, please update the changelog. Otherwise you can ignore this message. See: https://keepachangelog.com');
} }