Groups: allow simple moderation
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
b6b9fdb035
commit
7711553fd8
5 changed files with 214 additions and 23 deletions
|
@ -406,7 +406,7 @@ const groupKick = (groupId: string, accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch(groupKickRequest(groupId, accountId));
|
dispatch(groupKickRequest(groupId, accountId));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/groups/${groupId}/kick`, { account_ids: [accountId] })
|
return api(getState).post(`/api/v1/groups/${groupId}/kick`, { account_ids: [accountId] })
|
||||||
.then(() => dispatch(groupKickSuccess(groupId, accountId)))
|
.then(() => dispatch(groupKickSuccess(groupId, accountId)))
|
||||||
.catch(err => dispatch(groupKickFail(groupId, accountId, err)));
|
.catch(err => dispatch(groupKickFail(groupId, accountId, err)));
|
||||||
};
|
};
|
||||||
|
@ -506,7 +506,7 @@ const groupBlock = (groupId: string, accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch(groupBlockRequest(groupId, accountId));
|
dispatch(groupBlockRequest(groupId, accountId));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/groups/${groupId}/blocks`, { account_ids: [accountId] })
|
return api(getState).post(`/api/v1/groups/${groupId}/blocks`, { account_ids: [accountId] })
|
||||||
.then(() => dispatch(groupBlockSuccess(groupId, accountId)))
|
.then(() => dispatch(groupBlockSuccess(groupId, accountId)))
|
||||||
.catch(err => dispatch(groupBlockFail(groupId, accountId, err)));
|
.catch(err => dispatch(groupBlockFail(groupId, accountId, err)));
|
||||||
};
|
};
|
||||||
|
@ -534,7 +534,7 @@ const groupUnblock = (groupId: string, accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch(groupUnblockRequest(groupId, accountId));
|
dispatch(groupUnblockRequest(groupId, accountId));
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/groups/${groupId}/blocks?account_ids[]=${accountId}`)
|
return api(getState).delete(`/api/v1/groups/${groupId}/blocks?account_ids[]=${accountId}`)
|
||||||
.then(() => dispatch(groupUnblockSuccess(groupId, accountId)))
|
.then(() => dispatch(groupUnblockSuccess(groupId, accountId)))
|
||||||
.catch(err => dispatch(groupUnblockFail(groupId, accountId, err)));
|
.catch(err => dispatch(groupUnblockFail(groupId, accountId, err)));
|
||||||
};
|
};
|
||||||
|
@ -562,7 +562,7 @@ const groupPromoteAccount = (groupId: string, accountId: string, role: GroupRole
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch(groupPromoteAccountRequest(groupId, accountId));
|
dispatch(groupPromoteAccountRequest(groupId, accountId));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/groups/${groupId}/promote`, { account_ids: [accountId], role: role })
|
return api(getState).post(`/api/v1/groups/${groupId}/promote`, { account_ids: [accountId], role: role })
|
||||||
.then((response) => dispatch(groupPromoteAccountSuccess(groupId, accountId, response.data)))
|
.then((response) => dispatch(groupPromoteAccountSuccess(groupId, accountId, response.data)))
|
||||||
.catch(err => dispatch(groupPromoteAccountFail(groupId, accountId, err)));
|
.catch(err => dispatch(groupPromoteAccountFail(groupId, accountId, err)));
|
||||||
};
|
};
|
||||||
|
@ -591,7 +591,7 @@ const groupDemoteAccount = (groupId: string, accountId: string, role: GroupRole)
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch(groupDemoteAccountRequest(groupId, accountId));
|
dispatch(groupDemoteAccountRequest(groupId, accountId));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/groups/${groupId}/demote`, { account_ids: [accountId], role: role })
|
return api(getState).post(`/api/v1/groups/${groupId}/demote`, { account_ids: [accountId], role: role })
|
||||||
.then((response) => dispatch(groupDemoteAccountSuccess(groupId, accountId, response.data)))
|
.then((response) => dispatch(groupDemoteAccountSuccess(groupId, accountId, response.data)))
|
||||||
.catch(err => dispatch(groupDemoteAccountFail(groupId, accountId, err)));
|
.catch(err => dispatch(groupDemoteAccountFail(groupId, accountId, err)));
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,7 +31,7 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
||||||
{group.relationship?.role === 'admin' ? (
|
{group.relationship?.role === 'admin' ? (
|
||||||
<HStack space={1} alignItems='center'>
|
<HStack space={1} alignItems='center'>
|
||||||
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
|
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
|
||||||
<span><FormattedMessage id='group.role.owner' defaultMessage='Owner' /></span>
|
<span><FormattedMessage id='group.role.admin' defaultMessage='Admin' /></span>
|
||||||
</HStack>
|
</HStack>
|
||||||
) : group.relationship?.role === 'moderator' && (
|
) : group.relationship?.role === 'moderator' && (
|
||||||
<HStack space={1} alignItems='center'>
|
<HStack space={1} alignItems='center'>
|
||||||
|
|
|
@ -173,7 +173,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
{group.relationship?.role === 'admin' ? (
|
{group.relationship?.role === 'admin' ? (
|
||||||
<HStack space={1} alignItems='center'>
|
<HStack space={1} alignItems='center'>
|
||||||
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
|
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
|
||||||
<span><FormattedMessage id='group.role.owner' defaultMessage='Owner' /></span>
|
<span><FormattedMessage id='group.role.admin' defaultMessage='Admin' /></span>
|
||||||
</HStack>
|
</HStack>
|
||||||
) : group.relationship?.role === 'moderator' && (
|
) : group.relationship?.role === 'moderator' && (
|
||||||
<HStack space={1} alignItems='center'>
|
<HStack space={1} alignItems='center'>
|
||||||
|
|
|
@ -1,28 +1,210 @@
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { expandGroupMemberships, fetchGroup, fetchGroupMemberships } from 'soapbox/actions/groups';
|
import { expandGroupMemberships, fetchGroup, fetchGroupMemberships, groupBlock, groupDemoteAccount, groupKick, groupPromoteAccount } from 'soapbox/actions/groups';
|
||||||
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||||
import { CardHeader, CardTitle } from 'soapbox/components/ui';
|
import { CardHeader, CardTitle, HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui';
|
||||||
|
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
import AccountContainer from 'soapbox/containers/account-container';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
|
||||||
import PlaceholderAccount from '../placeholder/components/placeholder-account';
|
import PlaceholderAccount from '../placeholder/components/placeholder-account';
|
||||||
|
|
||||||
import type { List } from 'soapbox/reducers/group-memberships';
|
import type { Menu as MenuType } from 'soapbox/components/dropdown-menu';
|
||||||
|
import type { GroupRole, List } from 'soapbox/reducers/group-memberships';
|
||||||
|
import type { GroupRelationship } from 'soapbox/types/entities';
|
||||||
|
|
||||||
type RouteParams = { id: string };
|
type RouteParams = { id: string };
|
||||||
|
|
||||||
interface IGroupMembers {
|
const messages = defineMessages({
|
||||||
params: RouteParams,
|
adminSubheading: { id: 'group.admin_subheading', defaultMessage: 'Group administrators' },
|
||||||
|
moderatorSubheading: { id: 'group.moderator_subheading', defaultMessage: 'Group moderators' },
|
||||||
|
userSubheading: { id: 'group.user_subheading', defaultMessage: 'Users' },
|
||||||
|
groupModKick: { id: 'group.group_mod_kick', defaultMessage: 'Kick @{name} from group' },
|
||||||
|
groupModBlock: { id: 'group.group_mod_block', defaultMessage: 'Block @{name} from group' },
|
||||||
|
groupModPromoteAdmin: { id: 'group.group_mod_promote_admin', defaultMessage: 'Promote @{name} to group administrator' },
|
||||||
|
groupModPromoteMod: { id: 'group.group_mod_promote_mod', defaultMessage: 'Promote @{name} to group moderator' },
|
||||||
|
groupModDemote: { id: 'group.group_mod_demote', defaultMessage: 'Demote @{name}' },
|
||||||
|
kickFromGroupMessage: { id: 'confirmations.kick_from_group.message', defaultMessage: 'Are you sure you want to kick @{name} from this group?' },
|
||||||
|
kickConfirm: { id: 'confirmations.kick_from_group.confirm', defaultMessage: 'Kick' },
|
||||||
|
blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to block @{name} from interacting with this group?' },
|
||||||
|
blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' },
|
||||||
|
promoteConfirmMessage: { id: 'confirmations.promote_in_group.message', defaultMessage: 'Are you sure you want to promote @{name}? You will not be able to demote them.' },
|
||||||
|
promoteConfirm: { id: 'confirmations.promote_in_group.confirm', defaultMessage: 'Promote' },
|
||||||
|
kicked: { id: 'group.group_mod_kick.success', defaultMessage: 'Kicked @{name} from group' },
|
||||||
|
blocked: { id: 'group.group_mod_block.success', defaultMessage: 'Blocked @{name} from group' },
|
||||||
|
promotedToAdmin: { id: 'group.group_mod_promote_admin.success', defaultMessage: 'Promoted @{name} to group administrator' },
|
||||||
|
promotedToMod: { id: 'group.group_mod_promote_mod.success', defaultMessage: 'Promoted @{name} to group moderator' },
|
||||||
|
demotedToUser: { id: 'group.group_mod_demote.success', defaultMessage: 'Demoted @{name} to group user' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IGroupMember {
|
||||||
|
accountId: string
|
||||||
|
accountRole: GroupRole
|
||||||
|
groupId: string
|
||||||
|
relationship?: GroupRelationship
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const GroupMember: React.FC<IGroupMember> = ({ accountId, accountRole, groupId, relationship }) => {
|
||||||
adminSubheading: { id: 'groups.admin_subheading', defaultMessage: 'Group administrators' },
|
const intl = useIntl();
|
||||||
moderatorSubheading: { id: 'groups.moderator_subheading', defaultMessage: 'Group moderators' },
|
const dispatch = useAppDispatch();
|
||||||
userSubheading: { id: 'groups.user_subheading', defaultMessage: 'Users' },
|
|
||||||
|
const getAccount = useCallback(makeGetAccount(), []);
|
||||||
|
|
||||||
|
const account = useAppSelector((state) => getAccount(state, accountId));
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
const handleKickFromGroup = () => {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: intl.formatMessage(messages.kickFromGroupMessage, { name: account.username }),
|
||||||
|
confirm: intl.formatMessage(messages.kickConfirm),
|
||||||
|
onConfirm: () => dispatch(groupKick(groupId, account.id)).then(() =>
|
||||||
|
dispatch(snackbar.success(intl.formatMessage(messages.kicked, { name: account.acct }))),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlockFromGroup = () => {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: intl.formatMessage(messages.blockFromGroupMessage, { name: account.username }),
|
||||||
|
confirm: intl.formatMessage(messages.blockConfirm),
|
||||||
|
onConfirm: () => dispatch(groupBlock(groupId, account.id)).then(() =>
|
||||||
|
dispatch(snackbar.success(intl.formatMessage(messages.blocked, { name: account.acct }))),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPromote = (role: 'admin' | 'moderator', warning?: boolean) => {
|
||||||
|
if (warning) {
|
||||||
|
return dispatch(openModal('CONFIRM', {
|
||||||
|
message: intl.formatMessage(messages.promoteConfirmMessage, { name: account.username }),
|
||||||
|
confirm: intl.formatMessage(messages.promoteConfirm),
|
||||||
|
onConfirm: () => dispatch(groupPromoteAccount(groupId, account.id, role)).then(() =>
|
||||||
|
dispatch(snackbar.success(intl.formatMessage(role === 'admin' ? messages.promotedToAdmin : messages.promotedToMod, { name: account.acct }))),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
return dispatch(groupPromoteAccount(groupId, account.id, role)).then(() =>
|
||||||
|
dispatch(snackbar.success(intl.formatMessage(role === 'admin' ? messages.promotedToAdmin : messages.promotedToMod, { name: account.acct }))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePromoteToGroupAdmin = () => {
|
||||||
|
onPromote('admin', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePromoteToGroupMod = () => {
|
||||||
|
onPromote('moderator', relationship!.role === 'moderator');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDemote = () => {
|
||||||
|
dispatch(groupDemoteAccount(groupId, account.id, 'user')).then(() =>
|
||||||
|
dispatch(snackbar.success(intl.formatMessage(messages.demotedToUser, { name: account.acct }))),
|
||||||
|
).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeMenu = () => {
|
||||||
|
const menu: MenuType = [];
|
||||||
|
|
||||||
|
if (!relationship || !relationship.role) return menu;
|
||||||
|
|
||||||
|
if (['admin', 'moderator'].includes(relationship.role) && ['moderator', 'user'].includes(accountRole) && accountRole !== relationship.role) {
|
||||||
|
menu.push({
|
||||||
|
text: intl.formatMessage(messages.groupModKick, { name: account.username }),
|
||||||
|
icon: require('@tabler/icons/user-minus.svg'),
|
||||||
|
action: handleKickFromGroup,
|
||||||
});
|
});
|
||||||
|
menu.push({
|
||||||
|
text: intl.formatMessage(messages.groupModBlock, { name: account.username }),
|
||||||
|
icon: require('@tabler/icons/ban.svg'),
|
||||||
|
action: handleBlockFromGroup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relationship.role === 'admin' && accountRole !== 'admin' && account.acct === account.username) {
|
||||||
|
menu.push(null);
|
||||||
|
switch (accountRole) {
|
||||||
|
case 'moderator':
|
||||||
|
menu.push({
|
||||||
|
text: intl.formatMessage(messages.groupModPromoteAdmin, { name: account.username }),
|
||||||
|
icon: require('@tabler/icons/arrow-up-circle.svg'),
|
||||||
|
action: handlePromoteToGroupAdmin,
|
||||||
|
});
|
||||||
|
menu.push({
|
||||||
|
text: intl.formatMessage(messages.groupModDemote, { name: account.username }),
|
||||||
|
icon: require('@tabler/icons/arrow-down-circle.svg'),
|
||||||
|
action: handleDemote,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'user':
|
||||||
|
menu.push({
|
||||||
|
text: intl.formatMessage(messages.groupModPromoteMod, { name: account.username }),
|
||||||
|
icon: require('@tabler/icons/arrow-up-circle.svg'),
|
||||||
|
action: handlePromoteToGroupMod,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = makeMenu();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||||
|
<div className='w-full'>
|
||||||
|
<AccountContainer id={accountId} withRelationship={false} />
|
||||||
|
</div>
|
||||||
|
{menu.length > 0 && (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
src={require('@tabler/icons/dots.svg')}
|
||||||
|
theme='outlined'
|
||||||
|
className='px-2'
|
||||||
|
iconClassName='w-4 h-4'
|
||||||
|
children={null}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MenuList className='w-56'>
|
||||||
|
{menu.map((menuItem, idx) => {
|
||||||
|
if (typeof menuItem?.text === 'undefined') {
|
||||||
|
return <MenuDivider key={idx} />;
|
||||||
|
} else {
|
||||||
|
const Comp = (menuItem.action ? MenuItem : MenuLink) as any;
|
||||||
|
const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.newTab ? '_blank' : '_self' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp key={idx} {...itemProps} className='group'>
|
||||||
|
<HStack space={3} alignItems='center'>
|
||||||
|
{menuItem.icon && (
|
||||||
|
<SvgIcon src={menuItem.icon} className='h-5 w-5 text-gray-400 flex-none group-hover:text-gray-500' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='truncate'>{menuItem.text}</div>
|
||||||
|
</HStack>
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IGroupMembers {
|
||||||
|
params: RouteParams
|
||||||
|
}
|
||||||
|
|
||||||
const GroupMembers: React.FC<IGroupMembers> = (props) => {
|
const GroupMembers: React.FC<IGroupMembers> = (props) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -30,6 +212,7 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
|
||||||
|
|
||||||
const groupId = props.params.id;
|
const groupId = props.params.id;
|
||||||
|
|
||||||
|
const relationship = useAppSelector((state) => state.group_relationships.get(groupId));
|
||||||
const admins = useAppSelector((state) => state.group_memberships.admin.get(groupId));
|
const admins = useAppSelector((state) => state.group_memberships.admin.get(groupId));
|
||||||
const moderators = useAppSelector((state) => state.group_memberships.moderator.get(groupId));
|
const moderators = useAppSelector((state) => state.group_memberships.moderator.get(groupId));
|
||||||
const users = useAppSelector((state) => state.group_memberships.user.get(groupId));
|
const users = useAppSelector((state) => state.group_memberships.user.get(groupId));
|
||||||
|
@ -50,7 +233,7 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
|
||||||
handleLoadMore('user');
|
handleLoadMore('user');
|
||||||
}, 300, { leading: true }), []);
|
}, 300, { leading: true }), []);
|
||||||
|
|
||||||
const renderMemberships = (memberships: List | undefined, role: 'admin' | 'moderator' | 'user', handler: () => void) => {
|
const renderMemberships = (memberships: List | undefined, role: GroupRole, handler: () => void) => {
|
||||||
if (!memberships?.isLoading && !memberships?.items.count()) return;
|
if (!memberships?.isLoading && !memberships?.items.count()) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -68,7 +251,15 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
|
||||||
placeholderCount={3}
|
placeholderCount={3}
|
||||||
itemClassName='pb-4 last:pb-0'
|
itemClassName='pb-4 last:pb-0'
|
||||||
>
|
>
|
||||||
{memberships?.items?.map(accountId => <AccountContainer key={accountId} id={accountId} withRelationship={false} />)}
|
{memberships?.items?.map(accountId => (
|
||||||
|
<GroupMember
|
||||||
|
key={accountId}
|
||||||
|
accountId={accountId}
|
||||||
|
accountRole={role}
|
||||||
|
groupId={groupId}
|
||||||
|
relationship={relationship}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -701,17 +701,17 @@
|
||||||
"gdpr.message": "{siteTitle} korzysta z ciasteczek sesji, które są niezbędne dla działania strony.",
|
"gdpr.message": "{siteTitle} korzysta z ciasteczek sesji, które są niezbędne dla działania strony.",
|
||||||
"gdpr.title": "{siteTitle} korzysta z ciasteczek",
|
"gdpr.title": "{siteTitle} korzysta z ciasteczek",
|
||||||
"getting_started.open_source_notice": "{code_name} jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitLabie tutaj: {code_link} (v{code_version}).",
|
"getting_started.open_source_notice": "{code_name} jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitLabie tutaj: {code_link} (v{code_version}).",
|
||||||
"groups.admin_subheading": "Administratorzy grupy",
|
"group.admin_subheading": "Administratorzy grupy",
|
||||||
"groups.empty.title": "Brak grup",
|
"groups.empty.title": "Brak grup",
|
||||||
"groups.empty.subtitle": "Odkrywaj grupy do których możesz dołączyć lub utwórz własną.",
|
"groups.empty.subtitle": "Odkrywaj grupy do których możesz dołączyć lub utwórz własną.",
|
||||||
"groups.moderator_subheading": "Moderatorzy grupy",
|
"group.moderator_subheading": "Moderatorzy grupy",
|
||||||
"groups.user_subheading": "Członkowie grupy",
|
"group.user_subheading": "Członkowie grupy",
|
||||||
"group.header.alt": "Nagłówek grupy",
|
"group.header.alt": "Nagłówek grupy",
|
||||||
"group.join": "Dołącz do grupy",
|
"group.join": "Dołącz do grupy",
|
||||||
"group.leave": "Opuść grupę",
|
"group.leave": "Opuść grupę",
|
||||||
"group.manage": "Edytuj grupę",
|
"group.manage": "Edytuj grupę",
|
||||||
"group.request_join": "Poproś o dołączenie do grupy",
|
"group.request_join": "Poproś o dołączenie do grupy",
|
||||||
"group.role.owner": "Właściciel",
|
"group.role.admin": "Administrator",
|
||||||
"group.role.moderator": "Moderator",
|
"group.role.moderator": "Moderator",
|
||||||
"group.privacy.locked": "Prywatna",
|
"group.privacy.locked": "Prywatna",
|
||||||
"group.privacy.public": "Publiczna",
|
"group.privacy.public": "Publiczna",
|
||||||
|
|
Loading…
Reference in a new issue