mirror of
https://github.com/funamitech/mastodon
synced 2024-12-12 13:48:35 +09:00
Merge pull request #2761 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to d3f504245c
This commit is contained in:
commit
71375e984f
12
Gemfile.lock
12
Gemfile.lock
@ -100,8 +100,8 @@ GEM
|
|||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.947.0)
|
aws-partitions (1.949.0)
|
||||||
aws-sdk-core (3.199.0)
|
aws-sdk-core (3.200.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
@ -109,7 +109,7 @@ GEM
|
|||||||
aws-sdk-kms (1.87.0)
|
aws-sdk-kms (1.87.0)
|
||||||
aws-sdk-core (~> 3, >= 3.199.0)
|
aws-sdk-core (~> 3, >= 3.199.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.154.0)
|
aws-sdk-s3 (1.155.0)
|
||||||
aws-sdk-core (~> 3, >= 3.199.0)
|
aws-sdk-core (~> 3, >= 3.199.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
@ -696,7 +696,7 @@ GEM
|
|||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.3.0)
|
rexml (3.3.1)
|
||||||
strscan
|
strscan
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.2.1)
|
rouge (4.2.1)
|
||||||
@ -751,7 +751,7 @@ GEM
|
|||||||
rubocop-performance (1.21.1)
|
rubocop-performance (1.21.1)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.25.0)
|
rubocop-rails (2.25.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
@ -833,7 +833,7 @@ GEM
|
|||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (1.0.1)
|
terrapin (1.0.1)
|
||||||
climate_control
|
climate_control
|
||||||
test-prof (1.3.3)
|
test-prof (1.3.3.1)
|
||||||
thor (1.3.1)
|
thor (1.3.1)
|
||||||
tilt (2.3.0)
|
tilt (2.3.0)
|
||||||
timeout (0.4.1)
|
timeout (0.4.1)
|
||||||
|
@ -170,6 +170,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t
|
|||||||
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
||||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||||
|
export const expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
|
||||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||||
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||||
max_id: maxId,
|
max_id: maxId,
|
||||||
|
@ -44,6 +44,7 @@ export interface ApiPreviewCardJSON {
|
|||||||
type: string;
|
type: string;
|
||||||
author_name: string;
|
author_name: string;
|
||||||
author_url: string;
|
author_url: string;
|
||||||
|
author_account?: ApiAccountJSON;
|
||||||
provider_name: string;
|
provider_name: string;
|
||||||
provider_url: string;
|
provider_url: string;
|
||||||
html: string;
|
html: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useIdentity } from '@/flavours/glitch/identity_context';
|
import { useIdentity } from '@/flavours/glitch/identity_context';
|
||||||
import {
|
import {
|
||||||
@ -18,15 +18,11 @@ const messages = defineMessages({
|
|||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
|
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
|
||||||
cancel_follow_request: {
|
|
||||||
id: 'account.cancel_follow_request',
|
|
||||||
defaultMessage: 'Withdraw follow request',
|
|
||||||
},
|
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FollowButton: React.FC<{
|
export const FollowButton: React.FC<{
|
||||||
accountId: string;
|
accountId?: string;
|
||||||
}> = ({ accountId }) => {
|
}> = ({ accountId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -35,7 +31,7 @@ export const FollowButton: React.FC<{
|
|||||||
accountId ? state.accounts.get(accountId) : undefined,
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
);
|
);
|
||||||
const relationship = useAppSelector((state) =>
|
const relationship = useAppSelector((state) =>
|
||||||
state.relationships.get(accountId),
|
accountId ? state.relationships.get(accountId) : undefined,
|
||||||
);
|
);
|
||||||
const following = relationship?.following || relationship?.requested;
|
const following = relationship?.following || relationship?.requested;
|
||||||
|
|
||||||
@ -64,11 +60,28 @@ export const FollowButton: React.FC<{
|
|||||||
if (accountId === me) {
|
if (accountId === me) {
|
||||||
return;
|
return;
|
||||||
} else if (relationship.following || relationship.requested) {
|
} else if (relationship.following || relationship.requested) {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'CONFIRM',
|
||||||
|
modalProps: {
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.unfollow.message'
|
||||||
|
defaultMessage='Are you sure you want to unfollow {name}?'
|
||||||
|
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
confirm: intl.formatMessage(messages.unfollow),
|
||||||
|
onConfirm: () => {
|
||||||
dispatch(unfollowAccount(accountId));
|
dispatch(unfollowAccount(accountId));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(accountId));
|
dispatch(followAccount(accountId));
|
||||||
}
|
}
|
||||||
}, [dispatch, accountId, relationship, account, signedIn]);
|
}, [dispatch, intl, accountId, relationship, account, signedIn]);
|
||||||
|
|
||||||
let label;
|
let label;
|
||||||
|
|
||||||
@ -78,11 +91,9 @@ export const FollowButton: React.FC<{
|
|||||||
label = intl.formatMessage(messages.edit_profile);
|
label = intl.formatMessage(messages.edit_profile);
|
||||||
} else if (!relationship) {
|
} else if (!relationship) {
|
||||||
label = <LoadingIndicator />;
|
label = <LoadingIndicator />;
|
||||||
} else if (relationship.requested) {
|
|
||||||
label = intl.formatMessage(messages.cancel_follow_request);
|
|
||||||
} else if (!relationship.following && relationship.followed_by) {
|
} else if (!relationship.following && relationship.followed_by) {
|
||||||
label = intl.formatMessage(messages.followBack);
|
label = intl.formatMessage(messages.followBack);
|
||||||
} else if (relationship.following) {
|
} else if (relationship.following || relationship.requested) {
|
||||||
label = intl.formatMessage(messages.unfollow);
|
label = intl.formatMessage(messages.unfollow);
|
||||||
} else {
|
} else {
|
||||||
label = intl.formatMessage(messages.follow);
|
label = intl.formatMessage(messages.follow);
|
||||||
|
@ -17,7 +17,7 @@ import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
|||||||
|
|
||||||
export const HoverCardAccount = forwardRef<
|
export const HoverCardAccount = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ accountId: string }
|
{ accountId?: string }
|
||||||
>(({ accountId }, ref) => {
|
>(({ accountId }, ref) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ import { HoverCardAccount } from 'flavours/glitch/components/hover_card_account'
|
|||||||
import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
|
import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
|
||||||
|
|
||||||
const offset = [-12, 4] as OffsetValue;
|
const offset = [-12, 4] as OffsetValue;
|
||||||
const enterDelay = 650;
|
const enterDelay = 750;
|
||||||
const leaveDelay = 250;
|
const leaveDelay = 150;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
||||||
const isHoverCardAnchor = (element: HTMLElement) =>
|
const isHoverCardAnchor = (element: HTMLElement) =>
|
||||||
@ -23,50 +23,12 @@ export const HoverCardController: React.FC = () => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [accountId, setAccountId] = useState<string | undefined>();
|
const [accountId, setAccountId] = useState<string | undefined>();
|
||||||
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
||||||
const [setEnterTimeout, cancelEnterTimeout] = useTimeout();
|
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
|
||||||
|
const [setScrollTimeout] = useTimeout();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const handleAnchorMouseEnter = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
const { target } = e;
|
|
||||||
|
|
||||||
if (target instanceof HTMLElement && isHoverCardAnchor(target)) {
|
|
||||||
cancelLeaveTimeout();
|
|
||||||
|
|
||||||
setEnterTimeout(() => {
|
|
||||||
target.setAttribute('aria-describedby', 'hover-card');
|
|
||||||
setAnchor(target);
|
|
||||||
setOpen(true);
|
|
||||||
setAccountId(
|
|
||||||
target.getAttribute('data-hover-card-account') ?? undefined,
|
|
||||||
);
|
|
||||||
}, enterDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target === cardRef.current?.parentNode) {
|
|
||||||
cancelLeaveTimeout();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[cancelLeaveTimeout, setEnterTimeout, setOpen, setAccountId, setAnchor],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAnchorMouseLeave = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
if (e.target === anchor || e.target === cardRef.current?.parentNode) {
|
|
||||||
cancelEnterTimeout();
|
|
||||||
|
|
||||||
setLeaveTimeout(() => {
|
|
||||||
anchor?.removeAttribute('aria-describedby');
|
|
||||||
setOpen(false);
|
|
||||||
setAnchor(null);
|
|
||||||
}, leaveDelay);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[cancelEnterTimeout, setLeaveTimeout, setOpen, setAnchor, anchor],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
cancelEnterTimeout();
|
cancelEnterTimeout();
|
||||||
cancelLeaveTimeout();
|
cancelLeaveTimeout();
|
||||||
@ -79,22 +41,119 @@ export const HoverCardController: React.FC = () => {
|
|||||||
}, [handleClose, location]);
|
}, [handleClose, location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.addEventListener('mouseenter', handleAnchorMouseEnter, {
|
let isScrolling = false;
|
||||||
|
let currentAnchor: HTMLElement | null = null;
|
||||||
|
|
||||||
|
const open = (target: HTMLElement) => {
|
||||||
|
target.setAttribute('aria-describedby', 'hover-card');
|
||||||
|
setOpen(true);
|
||||||
|
setAnchor(target);
|
||||||
|
setAccountId(target.getAttribute('data-hover-card-account') ?? undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
currentAnchor?.removeAttribute('aria-describedby');
|
||||||
|
currentAnchor = null;
|
||||||
|
setOpen(false);
|
||||||
|
setAnchor(null);
|
||||||
|
setAccountId(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = (e: MouseEvent) => {
|
||||||
|
const { target } = e;
|
||||||
|
|
||||||
|
// We've exited the window
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've entered an anchor
|
||||||
|
if (!isScrolling && isHoverCardAnchor(target)) {
|
||||||
|
cancelLeaveTimeout();
|
||||||
|
|
||||||
|
currentAnchor?.removeAttribute('aria-describedby');
|
||||||
|
currentAnchor = target;
|
||||||
|
|
||||||
|
setEnterTimeout(() => {
|
||||||
|
open(target);
|
||||||
|
}, enterDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've entered the hover card
|
||||||
|
if (
|
||||||
|
!isScrolling &&
|
||||||
|
(target === currentAnchor || target === cardRef.current)
|
||||||
|
) {
|
||||||
|
cancelLeaveTimeout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (e: MouseEvent) => {
|
||||||
|
if (!currentAnchor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.target === currentAnchor || e.target === cardRef.current) {
|
||||||
|
cancelEnterTimeout();
|
||||||
|
|
||||||
|
setLeaveTimeout(() => {
|
||||||
|
close();
|
||||||
|
}, leaveDelay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrollEnd = () => {
|
||||||
|
isScrolling = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
isScrolling = true;
|
||||||
|
cancelEnterTimeout();
|
||||||
|
setScrollTimeout(handleScrollEnd, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = () => {
|
||||||
|
delayEnterTimeout(enterDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
document.body.addEventListener('mouseleave', handleAnchorMouseLeave, {
|
|
||||||
|
document.body.addEventListener('mousemove', handleMouseMove, {
|
||||||
|
passive: true,
|
||||||
|
capture: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseleave', handleMouseLeave, {
|
||||||
|
passive: true,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('scroll', handleScroll, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.removeEventListener('mouseenter', handleAnchorMouseEnter);
|
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
document.body.removeEventListener('mouseleave', handleAnchorMouseLeave);
|
document.body.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
document.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, [handleAnchorMouseEnter, handleAnchorMouseLeave]);
|
}, [
|
||||||
|
setEnterTimeout,
|
||||||
if (!accountId) return null;
|
setLeaveTimeout,
|
||||||
|
setScrollTimeout,
|
||||||
|
cancelEnterTimeout,
|
||||||
|
cancelLeaveTimeout,
|
||||||
|
delayEnterTimeout,
|
||||||
|
setOpen,
|
||||||
|
setAccountId,
|
||||||
|
setAnchor,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
|
@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
withCounters: PropTypes.bool,
|
withCounters: PropTypes.bool,
|
||||||
timelineId: PropTypes.string.isRequired,
|
timelineId: PropTypes.string.isRequired,
|
||||||
lastId: PropTypes.string,
|
lastId: PropTypes.string,
|
||||||
|
bindToDocument: PropTypes.bool,
|
||||||
regex: PropTypes.string,
|
regex: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
|||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
|
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
||||||
});
|
});
|
||||||
@ -43,7 +42,7 @@ const makeMapStateToProps = () => {
|
|||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onFollow (account) {
|
onFollow (account) {
|
||||||
if (account.getIn(['relationship', 'following'])) {
|
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'CONFIRM',
|
modalType: 'CONFIRM',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
@ -52,15 +51,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
|
||||||
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
|
|
||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
|||||||
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
||||||
|
|
||||||
const names = accounts.map(a => (
|
const names = accounts.map(a => (
|
||||||
<Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}>
|
<Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} data-hover-card-account={a.get('id')}>
|
||||||
<bdi>
|
<bdi>
|
||||||
<strong
|
<strong
|
||||||
className='display-name__html'
|
className='display-name__html'
|
||||||
|
@ -4,6 +4,8 @@ import { useState, useCallback } from 'react';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { Blurhash } from 'flavours/glitch/components/blurhash';
|
import { Blurhash } from 'flavours/glitch/components/blurhash';
|
||||||
@ -57,7 +59,7 @@ export const Story = ({
|
|||||||
|
|
||||||
<div className='story__details__shared'>
|
<div className='story__details__shared'>
|
||||||
{author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
|
{author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
|
||||||
{typeof sharedTimes === 'number' ? <span className='story__details__shared__pill'><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></span> : <Skeleton width='10ch' />}
|
{typeof sharedTimes === 'number' ? <Link className='story__details__shared__pill' to={`/links/${encodeURIComponent(url)}`}><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></Link> : <Skeleton width='10ch' />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import { useRef, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
||||||
|
import { expandLinkTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
|
import Column from 'flavours/glitch/components/column';
|
||||||
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
|
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
|
||||||
|
import type { Card } from 'flavours/glitch/models/status';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
export const LinkTimeline: React.FC<{
|
||||||
|
multiColumn: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const { url } = useParams<{ url: string }>();
|
||||||
|
const decodedUrl = url ? decodeURIComponent(url) : undefined;
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const columnRef = useRef<Column>(null);
|
||||||
|
const firstStatusId = useAppSelector((state) =>
|
||||||
|
decodedUrl
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
|
(state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
const story = useAppSelector((state) =>
|
||||||
|
firstStatusId
|
||||||
|
? (state.statuses.getIn([firstStatusId, 'card']) as Card)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHeaderClick = useCallback(() => {
|
||||||
|
columnRef.current?.scrollTop();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(
|
||||||
|
(maxId: string) => {
|
||||||
|
dispatch(expandLinkTimeline(decodedUrl, { maxId }));
|
||||||
|
},
|
||||||
|
[dispatch, decodedUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(expandLinkTimeline(decodedUrl));
|
||||||
|
}, [dispatch, decodedUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={story?.title}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='explore'
|
||||||
|
iconComponent={ExploreIcon}
|
||||||
|
title={story?.title}
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
timelineId={`link:${decodedUrl}`}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
trackScroll
|
||||||
|
scrollKey={`link_timeline-${decodedUrl}`}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
regex={undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{story?.title}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default LinkTimeline;
|
@ -58,6 +58,7 @@ import {
|
|||||||
FavouritedStatuses,
|
FavouritedStatuses,
|
||||||
BookmarkedStatuses,
|
BookmarkedStatuses,
|
||||||
FollowedTags,
|
FollowedTags,
|
||||||
|
LinkTimeline,
|
||||||
ListTimeline,
|
ListTimeline,
|
||||||
Blocks,
|
Blocks,
|
||||||
DomainBlocks,
|
DomainBlocks,
|
||||||
@ -211,6 +212,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
|
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
|
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
|
||||||
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
||||||
@ -649,7 +651,7 @@ class UI extends PureComponent {
|
|||||||
|
|
||||||
{layout !== 'mobile' && <PictureInPicture />}
|
{layout !== 'mobile' && <PictureInPicture />}
|
||||||
<NotificationsContainer />
|
<NotificationsContainer />
|
||||||
{/* Temporarily disabled while upstream improves the issue */ null && <HoverCardController />}
|
<HoverCardController />
|
||||||
<LoadingBarContainer className='loading-bar' />
|
<LoadingBarContainer className='loading-bar' />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||||
|
@ -213,3 +213,7 @@ export function NotificationRequests () {
|
|||||||
export function NotificationRequest () {
|
export function NotificationRequest () {
|
||||||
return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request');
|
return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LinkTimeline () {
|
||||||
|
return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline');
|
||||||
|
}
|
||||||
|
@ -2,19 +2,34 @@ import { useRef, useCallback, useEffect } from 'react';
|
|||||||
|
|
||||||
export const useTimeout = () => {
|
export const useTimeout = () => {
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const callbackRef = useRef<() => void>();
|
||||||
|
|
||||||
const set = useCallback((callback: () => void, delay: number) => {
|
const set = useCallback((callback: () => void, delay: number) => {
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callbackRef.current = callback;
|
||||||
timeoutRef.current = setTimeout(callback, delay);
|
timeoutRef.current = setTimeout(callback, delay);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const delay = useCallback((delay: number) => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callbackRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(callbackRef.current, delay);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
const cancel = useCallback(() => {
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
timeoutRef.current = undefined;
|
timeoutRef.current = undefined;
|
||||||
|
callbackRef.current = undefined;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -25,5 +40,5 @@ export const useTimeout = () => {
|
|||||||
[cancel],
|
[cancel],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [set, cancel] as const;
|
return [set, cancel, delay] as const;
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiPreviewCardJSON } from 'flavours/glitch/api_types/statuses';
|
||||||
|
|
||||||
export type { StatusVisibility } from 'flavours/glitch/api_types/statuses';
|
export type { StatusVisibility } from 'flavours/glitch/api_types/statuses';
|
||||||
|
|
||||||
// Temporary until we type it correctly
|
// Temporary until we type it correctly
|
||||||
export type Status = Immutable.Map<string, unknown>;
|
export type Status = Immutable.Map<string, unknown>;
|
||||||
|
|
||||||
|
type CardShape = Required<ApiPreviewCardJSON>;
|
||||||
|
|
||||||
|
export type Card = RecordOf<CardShape>;
|
||||||
|
@ -11022,12 +11022,14 @@ noscript {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.verified {
|
&.verified {
|
||||||
dd {
|
dd {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -2,19 +2,34 @@ import { useRef, useCallback, useEffect } from 'react';
|
|||||||
|
|
||||||
export const useTimeout = () => {
|
export const useTimeout = () => {
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const callbackRef = useRef<() => void>();
|
||||||
|
|
||||||
const set = useCallback((callback: () => void, delay: number) => {
|
const set = useCallback((callback: () => void, delay: number) => {
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callbackRef.current = callback;
|
||||||
timeoutRef.current = setTimeout(callback, delay);
|
timeoutRef.current = setTimeout(callback, delay);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const delay = useCallback((delay: number) => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callbackRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(callbackRef.current, delay);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
const cancel = useCallback(() => {
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
timeoutRef.current = undefined;
|
timeoutRef.current = undefined;
|
||||||
|
callbackRef.current = undefined;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -25,5 +40,5 @@ export const useTimeout = () => {
|
|||||||
[cancel],
|
[cancel],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [set, cancel] as const;
|
return [set, cancel, delay] as const;
|
||||||
};
|
};
|
||||||
|
@ -158,6 +158,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t
|
|||||||
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
||||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||||
|
export const expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
|
||||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||||
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||||
max_id: maxId,
|
max_id: maxId,
|
||||||
|
@ -44,6 +44,7 @@ export interface ApiPreviewCardJSON {
|
|||||||
type: string;
|
type: string;
|
||||||
author_name: string;
|
author_name: string;
|
||||||
author_url: string;
|
author_url: string;
|
||||||
|
author_account?: ApiAccountJSON;
|
||||||
provider_name: string;
|
provider_name: string;
|
||||||
provider_url: string;
|
provider_url: string;
|
||||||
html: string;
|
html: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useIdentity } from '@/mastodon/identity_context';
|
import { useIdentity } from '@/mastodon/identity_context';
|
||||||
import {
|
import {
|
||||||
@ -19,15 +19,11 @@ const messages = defineMessages({
|
|||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
|
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
|
||||||
mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
|
mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
|
||||||
cancel_follow_request: {
|
|
||||||
id: 'account.cancel_follow_request',
|
|
||||||
defaultMessage: 'Withdraw follow request',
|
|
||||||
},
|
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FollowButton: React.FC<{
|
export const FollowButton: React.FC<{
|
||||||
accountId: string;
|
accountId?: string;
|
||||||
}> = ({ accountId }) => {
|
}> = ({ accountId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -36,7 +32,7 @@ export const FollowButton: React.FC<{
|
|||||||
accountId ? state.accounts.get(accountId) : undefined,
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
);
|
);
|
||||||
const relationship = useAppSelector((state) =>
|
const relationship = useAppSelector((state) =>
|
||||||
state.relationships.get(accountId),
|
accountId ? state.relationships.get(accountId) : undefined,
|
||||||
);
|
);
|
||||||
const following = relationship?.following || relationship?.requested;
|
const following = relationship?.following || relationship?.requested;
|
||||||
|
|
||||||
@ -65,11 +61,28 @@ export const FollowButton: React.FC<{
|
|||||||
if (accountId === me) {
|
if (accountId === me) {
|
||||||
return;
|
return;
|
||||||
} else if (relationship.following || relationship.requested) {
|
} else if (relationship.following || relationship.requested) {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'CONFIRM',
|
||||||
|
modalProps: {
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.unfollow.message'
|
||||||
|
defaultMessage='Are you sure you want to unfollow {name}?'
|
||||||
|
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
confirm: intl.formatMessage(messages.unfollow),
|
||||||
|
onConfirm: () => {
|
||||||
dispatch(unfollowAccount(accountId));
|
dispatch(unfollowAccount(accountId));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(accountId));
|
dispatch(followAccount(accountId));
|
||||||
}
|
}
|
||||||
}, [dispatch, accountId, relationship, account, signedIn]);
|
}, [dispatch, intl, accountId, relationship, account, signedIn]);
|
||||||
|
|
||||||
let label;
|
let label;
|
||||||
|
|
||||||
@ -79,13 +92,11 @@ export const FollowButton: React.FC<{
|
|||||||
label = intl.formatMessage(messages.edit_profile);
|
label = intl.formatMessage(messages.edit_profile);
|
||||||
} else if (!relationship) {
|
} else if (!relationship) {
|
||||||
label = <LoadingIndicator />;
|
label = <LoadingIndicator />;
|
||||||
} else if (relationship.requested) {
|
|
||||||
label = intl.formatMessage(messages.cancel_follow_request);
|
|
||||||
} else if (relationship.following && relationship.followed_by) {
|
} else if (relationship.following && relationship.followed_by) {
|
||||||
label = intl.formatMessage(messages.mutual);
|
label = intl.formatMessage(messages.mutual);
|
||||||
} else if (!relationship.following && relationship.followed_by) {
|
} else if (!relationship.following && relationship.followed_by) {
|
||||||
label = intl.formatMessage(messages.followBack);
|
label = intl.formatMessage(messages.followBack);
|
||||||
} else if (relationship.following) {
|
} else if (relationship.following || relationship.requested) {
|
||||||
label = intl.formatMessage(messages.unfollow);
|
label = intl.formatMessage(messages.unfollow);
|
||||||
} else {
|
} else {
|
||||||
label = intl.formatMessage(messages.follow);
|
label = intl.formatMessage(messages.follow);
|
||||||
|
@ -17,7 +17,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
|||||||
|
|
||||||
export const HoverCardAccount = forwardRef<
|
export const HoverCardAccount = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ accountId: string }
|
{ accountId?: string }
|
||||||
>(({ accountId }, ref) => {
|
>(({ accountId }, ref) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
|||||||
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
||||||
|
|
||||||
const offset = [-12, 4] as OffsetValue;
|
const offset = [-12, 4] as OffsetValue;
|
||||||
const enterDelay = 650;
|
const enterDelay = 750;
|
||||||
const leaveDelay = 250;
|
const leaveDelay = 150;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
||||||
const isHoverCardAnchor = (element: HTMLElement) =>
|
const isHoverCardAnchor = (element: HTMLElement) =>
|
||||||
@ -23,50 +23,12 @@ export const HoverCardController: React.FC = () => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [accountId, setAccountId] = useState<string | undefined>();
|
const [accountId, setAccountId] = useState<string | undefined>();
|
||||||
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
||||||
const [setEnterTimeout, cancelEnterTimeout] = useTimeout();
|
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
|
||||||
|
const [setScrollTimeout] = useTimeout();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const handleAnchorMouseEnter = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
const { target } = e;
|
|
||||||
|
|
||||||
if (target instanceof HTMLElement && isHoverCardAnchor(target)) {
|
|
||||||
cancelLeaveTimeout();
|
|
||||||
|
|
||||||
setEnterTimeout(() => {
|
|
||||||
target.setAttribute('aria-describedby', 'hover-card');
|
|
||||||
setAnchor(target);
|
|
||||||
setOpen(true);
|
|
||||||
setAccountId(
|
|
||||||
target.getAttribute('data-hover-card-account') ?? undefined,
|
|
||||||
);
|
|
||||||
}, enterDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target === cardRef.current?.parentNode) {
|
|
||||||
cancelLeaveTimeout();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[cancelLeaveTimeout, setEnterTimeout, setOpen, setAccountId, setAnchor],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAnchorMouseLeave = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
if (e.target === anchor || e.target === cardRef.current?.parentNode) {
|
|
||||||
cancelEnterTimeout();
|
|
||||||
|
|
||||||
setLeaveTimeout(() => {
|
|
||||||
anchor?.removeAttribute('aria-describedby');
|
|
||||||
setOpen(false);
|
|
||||||
setAnchor(null);
|
|
||||||
}, leaveDelay);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[cancelEnterTimeout, setLeaveTimeout, setOpen, setAnchor, anchor],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
cancelEnterTimeout();
|
cancelEnterTimeout();
|
||||||
cancelLeaveTimeout();
|
cancelLeaveTimeout();
|
||||||
@ -79,22 +41,119 @@ export const HoverCardController: React.FC = () => {
|
|||||||
}, [handleClose, location]);
|
}, [handleClose, location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.addEventListener('mouseenter', handleAnchorMouseEnter, {
|
let isScrolling = false;
|
||||||
|
let currentAnchor: HTMLElement | null = null;
|
||||||
|
|
||||||
|
const open = (target: HTMLElement) => {
|
||||||
|
target.setAttribute('aria-describedby', 'hover-card');
|
||||||
|
setOpen(true);
|
||||||
|
setAnchor(target);
|
||||||
|
setAccountId(target.getAttribute('data-hover-card-account') ?? undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
currentAnchor?.removeAttribute('aria-describedby');
|
||||||
|
currentAnchor = null;
|
||||||
|
setOpen(false);
|
||||||
|
setAnchor(null);
|
||||||
|
setAccountId(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = (e: MouseEvent) => {
|
||||||
|
const { target } = e;
|
||||||
|
|
||||||
|
// We've exited the window
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've entered an anchor
|
||||||
|
if (!isScrolling && isHoverCardAnchor(target)) {
|
||||||
|
cancelLeaveTimeout();
|
||||||
|
|
||||||
|
currentAnchor?.removeAttribute('aria-describedby');
|
||||||
|
currentAnchor = target;
|
||||||
|
|
||||||
|
setEnterTimeout(() => {
|
||||||
|
open(target);
|
||||||
|
}, enterDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've entered the hover card
|
||||||
|
if (
|
||||||
|
!isScrolling &&
|
||||||
|
(target === currentAnchor || target === cardRef.current)
|
||||||
|
) {
|
||||||
|
cancelLeaveTimeout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (e: MouseEvent) => {
|
||||||
|
if (!currentAnchor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.target === currentAnchor || e.target === cardRef.current) {
|
||||||
|
cancelEnterTimeout();
|
||||||
|
|
||||||
|
setLeaveTimeout(() => {
|
||||||
|
close();
|
||||||
|
}, leaveDelay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrollEnd = () => {
|
||||||
|
isScrolling = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
isScrolling = true;
|
||||||
|
cancelEnterTimeout();
|
||||||
|
setScrollTimeout(handleScrollEnd, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = () => {
|
||||||
|
delayEnterTimeout(enterDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
document.body.addEventListener('mouseleave', handleAnchorMouseLeave, {
|
|
||||||
|
document.body.addEventListener('mousemove', handleMouseMove, {
|
||||||
|
passive: true,
|
||||||
|
capture: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseleave', handleMouseLeave, {
|
||||||
|
passive: true,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('scroll', handleScroll, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.removeEventListener('mouseenter', handleAnchorMouseEnter);
|
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
document.body.removeEventListener('mouseleave', handleAnchorMouseLeave);
|
document.body.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
document.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, [handleAnchorMouseEnter, handleAnchorMouseLeave]);
|
}, [
|
||||||
|
setEnterTimeout,
|
||||||
if (!accountId) return null;
|
setLeaveTimeout,
|
||||||
|
setScrollTimeout,
|
||||||
|
cancelEnterTimeout,
|
||||||
|
cancelLeaveTimeout,
|
||||||
|
delayEnterTimeout,
|
||||||
|
setOpen,
|
||||||
|
setAccountId,
|
||||||
|
setAnchor,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
|
@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
withCounters: PropTypes.bool,
|
withCounters: PropTypes.bool,
|
||||||
timelineId: PropTypes.string,
|
timelineId: PropTypes.string,
|
||||||
lastId: PropTypes.string,
|
lastId: PropTypes.string,
|
||||||
|
bindToDocument: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -94,7 +94,7 @@ const messageForFollowButton = relationship => {
|
|||||||
return messages.mutual;
|
return messages.mutual;
|
||||||
} else if (!relationship.get('following') && relationship.get('followed_by')) {
|
} else if (!relationship.get('following') && relationship.get('followed_by')) {
|
||||||
return messages.followBack;
|
return messages.followBack;
|
||||||
} else if (relationship.get('following')) {
|
} else if (relationship.get('following') || relationship.get('requested')) {
|
||||||
return messages.unfollow;
|
return messages.unfollow;
|
||||||
} else {
|
} else {
|
||||||
return messages.follow;
|
return messages.follow;
|
||||||
@ -291,10 +291,8 @@ class Header extends ImmutablePureComponent {
|
|||||||
if (me !== account.get('id')) {
|
if (me !== account.get('id')) {
|
||||||
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
||||||
actionBtn = <Button disabled><LoadingIndicator /></Button>;
|
actionBtn = <Button disabled><LoadingIndicator /></Button>;
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
|
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
|
||||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
|||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
|
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
||||||
});
|
});
|
||||||
@ -45,7 +44,7 @@ const makeMapStateToProps = () => {
|
|||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onFollow (account) {
|
onFollow (account) {
|
||||||
if (account.getIn(['relationship', 'following'])) {
|
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'CONFIRM',
|
modalType: 'CONFIRM',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
@ -54,15 +53,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
|
||||||
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
|
|
||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
|||||||
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
||||||
|
|
||||||
const names = accounts.map(a => (
|
const names = accounts.map(a => (
|
||||||
<Link to={`/@${a.get('acct')}`} key={a.get('id')} title={a.get('acct')}>
|
<Link to={`/@${a.get('acct')}`} key={a.get('id')} data-hover-card-account={a.get('id')}>
|
||||||
<bdi>
|
<bdi>
|
||||||
<strong
|
<strong
|
||||||
className='display-name__html'
|
className='display-name__html'
|
||||||
|
@ -4,6 +4,8 @@ import { useState, useCallback } from 'react';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { Blurhash } from 'mastodon/components/blurhash';
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
@ -57,7 +59,7 @@ export const Story = ({
|
|||||||
|
|
||||||
<div className='story__details__shared'>
|
<div className='story__details__shared'>
|
||||||
{author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
|
{author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
|
||||||
{typeof sharedTimes === 'number' ? <span className='story__details__shared__pill'><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></span> : <Skeleton width='10ch' />}
|
{typeof sharedTimes === 'number' ? <Link className='story__details__shared__pill' to={`/links/${encodeURIComponent(url)}`}><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></Link> : <Skeleton width='10ch' />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
76
app/javascript/mastodon/features/link_timeline/index.tsx
Normal file
76
app/javascript/mastodon/features/link_timeline/index.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { useRef, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
||||||
|
import { expandLinkTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import Column from 'mastodon/components/column';
|
||||||
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
|
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
|
||||||
|
import type { Card } from 'mastodon/models/status';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const LinkTimeline: React.FC<{
|
||||||
|
multiColumn: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const { url } = useParams<{ url: string }>();
|
||||||
|
const decodedUrl = url ? decodeURIComponent(url) : undefined;
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const columnRef = useRef<Column>(null);
|
||||||
|
const firstStatusId = useAppSelector((state) =>
|
||||||
|
decodedUrl
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
|
(state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
const story = useAppSelector((state) =>
|
||||||
|
firstStatusId
|
||||||
|
? (state.statuses.getIn([firstStatusId, 'card']) as Card)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHeaderClick = useCallback(() => {
|
||||||
|
columnRef.current?.scrollTop();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(
|
||||||
|
(maxId: string) => {
|
||||||
|
dispatch(expandLinkTimeline(decodedUrl, { maxId }));
|
||||||
|
},
|
||||||
|
[dispatch, decodedUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(expandLinkTimeline(decodedUrl));
|
||||||
|
}, [dispatch, decodedUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={story?.title}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='explore'
|
||||||
|
iconComponent={ExploreIcon}
|
||||||
|
title={story?.title}
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
timelineId={`link:${decodedUrl}`}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
trackScroll
|
||||||
|
scrollKey={`link_timeline-${decodedUrl}`}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{story?.title}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default LinkTimeline;
|
@ -56,6 +56,7 @@ import {
|
|||||||
FavouritedStatuses,
|
FavouritedStatuses,
|
||||||
BookmarkedStatuses,
|
BookmarkedStatuses,
|
||||||
FollowedTags,
|
FollowedTags,
|
||||||
|
LinkTimeline,
|
||||||
ListTimeline,
|
ListTimeline,
|
||||||
Blocks,
|
Blocks,
|
||||||
DomainBlocks,
|
DomainBlocks,
|
||||||
@ -202,6 +203,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
|
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
|
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
|
||||||
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
||||||
|
@ -201,3 +201,7 @@ export function NotificationRequests () {
|
|||||||
export function NotificationRequest () {
|
export function NotificationRequest () {
|
||||||
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
|
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LinkTimeline () {
|
||||||
|
return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
|
||||||
|
}
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
"account.follow_back": "Heuliañ d'ho tro",
|
"account.follow_back": "Heuliañ d'ho tro",
|
||||||
"account.followers": "Tud koumanantet",
|
"account.followers": "Tud koumanantet",
|
||||||
"account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.",
|
"account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} heulier} two {{counter} heulier} few {{counter} heulier} many {{counter} heulier} other {{counter} heulier}}",
|
||||||
"account.following": "Koumanantoù",
|
"account.following": "Koumanantoù",
|
||||||
"account.follows.empty": "An implijer·ez-mañ na heul den ebet.",
|
"account.follows.empty": "An implijer·ez-mañ na heul den ebet.",
|
||||||
"account.go_to_profile": "Gwelet ar profil",
|
"account.go_to_profile": "Gwelet ar profil",
|
||||||
@ -60,6 +61,7 @@
|
|||||||
"account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ",
|
"account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ",
|
||||||
"account.share": "Skignañ profil @{name}",
|
"account.share": "Skignañ profil @{name}",
|
||||||
"account.show_reblogs": "Diskouez skignadennoù @{name}",
|
"account.show_reblogs": "Diskouez skignadennoù @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} embannadur} two {{counter} embannadur} few {{counter} embannadur} many {{counter} embannadur} other {{counter} embannadur}}",
|
||||||
"account.unblock": "Diverzañ @{name}",
|
"account.unblock": "Diverzañ @{name}",
|
||||||
"account.unblock_domain": "Diverzañ an domani {domain}",
|
"account.unblock_domain": "Diverzañ an domani {domain}",
|
||||||
"account.unblock_short": "Distankañ",
|
"account.unblock_short": "Distankañ",
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"account.block_short": "Zablokovat",
|
"account.block_short": "Zablokovat",
|
||||||
"account.blocked": "Blokovaný",
|
"account.blocked": "Blokovaný",
|
||||||
"account.browse_more_on_origin_server": "Více na původním profilu",
|
"account.browse_more_on_origin_server": "Více na původním profilu",
|
||||||
"account.cancel_follow_request": "Zrušit žádost o sledování",
|
"account.cancel_follow_request": "Zrušit sledování",
|
||||||
"account.copy": "Kopírovat odkaz na profil",
|
"account.copy": "Kopírovat odkaz na profil",
|
||||||
"account.direct": "Soukromě zmínit @{name}",
|
"account.direct": "Soukromě zmínit @{name}",
|
||||||
"account.disable_notifications": "Přestat mě upozorňovat, když @{name} zveřejní příspěvek",
|
"account.disable_notifications": "Přestat mě upozorňovat, když @{name} zveřejní příspěvek",
|
||||||
@ -35,7 +35,9 @@
|
|||||||
"account.follow_back": "Také sledovat",
|
"account.follow_back": "Také sledovat",
|
||||||
"account.followers": "Sledující",
|
"account.followers": "Sledující",
|
||||||
"account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.",
|
"account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} sledující} few {{counter} sledující} many {{counter} sledujících} other {{counter} sledujících}}",
|
||||||
"account.following": "Sledujete",
|
"account.following": "Sledujete",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} sledovaný} few {{counter} sledovaní} many {{counter} sledovaných} other {{counter} sledovaných}}",
|
||||||
"account.follows.empty": "Tento uživatel zatím nikoho nesleduje.",
|
"account.follows.empty": "Tento uživatel zatím nikoho nesleduje.",
|
||||||
"account.go_to_profile": "Přejít na profil",
|
"account.go_to_profile": "Přejít na profil",
|
||||||
"account.hide_reblogs": "Skrýt boosty od @{name}",
|
"account.hide_reblogs": "Skrýt boosty od @{name}",
|
||||||
@ -61,6 +63,7 @@
|
|||||||
"account.requested_follow": "{name} tě požádal o sledování",
|
"account.requested_follow": "{name} tě požádal o sledování",
|
||||||
"account.share": "Sdílet profil @{name}",
|
"account.share": "Sdílet profil @{name}",
|
||||||
"account.show_reblogs": "Zobrazit boosty od @{name}",
|
"account.show_reblogs": "Zobrazit boosty od @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
|
||||||
"account.unblock": "Odblokovat @{name}",
|
"account.unblock": "Odblokovat @{name}",
|
||||||
"account.unblock_domain": "Odblokovat doménu {domain}",
|
"account.unblock_domain": "Odblokovat doménu {domain}",
|
||||||
"account.unblock_short": "Odblokovat",
|
"account.unblock_short": "Odblokovat",
|
||||||
@ -75,9 +78,9 @@
|
|||||||
"admin.dashboard.retention.average": "Průměr",
|
"admin.dashboard.retention.average": "Průměr",
|
||||||
"admin.dashboard.retention.cohort": "Měsíc registrace",
|
"admin.dashboard.retention.cohort": "Měsíc registrace",
|
||||||
"admin.dashboard.retention.cohort_size": "Noví uživatelé",
|
"admin.dashboard.retention.cohort_size": "Noví uživatelé",
|
||||||
"admin.impact_report.instance_accounts": "Profily účtů, které by odstranily",
|
"admin.impact_report.instance_accounts": "Profily účtů, které by byli odstaněny",
|
||||||
"admin.impact_report.instance_followers": "Sledovatelé, o které by naši uživatelé přišli",
|
"admin.impact_report.instance_followers": "Sledující, o které by naši uživatelé přišli",
|
||||||
"admin.impact_report.instance_follows": "Následovníci jejich uživatelé by ztratili",
|
"admin.impact_report.instance_follows": "Sledující, o které by naši uživatelé přišli",
|
||||||
"admin.impact_report.title": "Shrnutí dopadu",
|
"admin.impact_report.title": "Shrnutí dopadu",
|
||||||
"alert.rate_limited.message": "Zkuste to prosím znovu po {retry_time, time, medium}.",
|
"alert.rate_limited.message": "Zkuste to prosím znovu po {retry_time, time, medium}.",
|
||||||
"alert.rate_limited.title": "Spojení omezena",
|
"alert.rate_limited.title": "Spojení omezena",
|
||||||
@ -86,7 +89,7 @@
|
|||||||
"announcement.announcement": "Oznámení",
|
"announcement.announcement": "Oznámení",
|
||||||
"attachments_list.unprocessed": "(nezpracováno)",
|
"attachments_list.unprocessed": "(nezpracováno)",
|
||||||
"audio.hide": "Skrýt zvuk",
|
"audio.hide": "Skrýt zvuk",
|
||||||
"block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou být stále viditelné pro nepřihlášené uživatele.",
|
"block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou stále být viditelné pro nepřihlášené uživatele.",
|
||||||
"block_modal.show_less": "Zobrazit méně",
|
"block_modal.show_less": "Zobrazit méně",
|
||||||
"block_modal.show_more": "Zobrazit více",
|
"block_modal.show_more": "Zobrazit více",
|
||||||
"block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.",
|
"block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.",
|
||||||
@ -411,6 +414,8 @@
|
|||||||
"limited_account_hint.action": "Přesto profil zobrazit",
|
"limited_account_hint.action": "Přesto profil zobrazit",
|
||||||
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
|
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
|
||||||
"link_preview.author": "Podle {name}",
|
"link_preview.author": "Podle {name}",
|
||||||
|
"link_preview.more_from_author": "Více od {name}",
|
||||||
|
"link_preview.shares": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
|
||||||
"lists.account.add": "Přidat do seznamu",
|
"lists.account.add": "Přidat do seznamu",
|
||||||
"lists.account.remove": "Odebrat ze seznamu",
|
"lists.account.remove": "Odebrat ze seznamu",
|
||||||
"lists.delete": "Smazat seznam",
|
"lists.delete": "Smazat seznam",
|
||||||
@ -691,8 +696,11 @@
|
|||||||
"server_banner.about_active_users": "Lidé používající tento server během posledních 30 dní (měsíční aktivní uživatelé)",
|
"server_banner.about_active_users": "Lidé používající tento server během posledních 30 dní (měsíční aktivní uživatelé)",
|
||||||
"server_banner.active_users": "aktivní uživatelé",
|
"server_banner.active_users": "aktivní uživatelé",
|
||||||
"server_banner.administered_by": "Spravováno:",
|
"server_banner.administered_by": "Spravováno:",
|
||||||
|
"server_banner.is_one_of_many": "{domain} je jedním z mnoha Mastodon serverů, které můžete použít k účasti na fediversu.",
|
||||||
"server_banner.server_stats": "Statistiky serveru:",
|
"server_banner.server_stats": "Statistiky serveru:",
|
||||||
"sign_in_banner.create_account": "Vytvořit účet",
|
"sign_in_banner.create_account": "Vytvořit účet",
|
||||||
|
"sign_in_banner.follow_anyone": "Sledujte kohokoli napříč fediversem a uvidíte vše v chronologickém pořadí. Bez algoritmů, reklam a clickbaitu.",
|
||||||
|
"sign_in_banner.mastodon_is": "Mastodon je ten nejlepší způsob, jak udržet krok s tím, co se právě děje.",
|
||||||
"sign_in_banner.sign_in": "Přihlásit se",
|
"sign_in_banner.sign_in": "Přihlásit se",
|
||||||
"sign_in_banner.sso_redirect": "Přihlášení nebo Registrace",
|
"sign_in_banner.sso_redirect": "Přihlášení nebo Registrace",
|
||||||
"status.admin_account": "Otevřít moderátorské rozhraní pro @{name}",
|
"status.admin_account": "Otevřít moderátorské rozhraní pro @{name}",
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
"account.followers.empty": "Diesem Profil folgt noch niemand.",
|
"account.followers.empty": "Diesem Profil folgt noch niemand.",
|
||||||
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}",
|
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}",
|
||||||
"account.following": "Folge ich",
|
"account.following": "Folge ich",
|
||||||
"account.following_counter": "{count, plural, one {{counter} folge ich} other {{counter} folge ich}}",
|
"account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
|
||||||
"account.follows.empty": "Dieses Profil folgt noch niemandem.",
|
"account.follows.empty": "Dieses Profil folgt noch niemandem.",
|
||||||
"account.go_to_profile": "Profil aufrufen",
|
"account.go_to_profile": "Profil aufrufen",
|
||||||
"account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden",
|
"account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden",
|
||||||
|
@ -35,7 +35,9 @@
|
|||||||
"account.follow_back": "Fylg aftur",
|
"account.follow_back": "Fylg aftur",
|
||||||
"account.followers": "Fylgjarar",
|
"account.followers": "Fylgjarar",
|
||||||
"account.followers.empty": "Ongar fylgjarar enn.",
|
"account.followers.empty": "Ongar fylgjarar enn.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} fylgjari} other {{counter} fylgjarar}}",
|
||||||
"account.following": "Fylgir",
|
"account.following": "Fylgir",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} fylgir} other {{counter} fylgja}}",
|
||||||
"account.follows.empty": "Hesin brúkari fylgir ongum enn.",
|
"account.follows.empty": "Hesin brúkari fylgir ongum enn.",
|
||||||
"account.go_to_profile": "Far til vanga",
|
"account.go_to_profile": "Far til vanga",
|
||||||
"account.hide_reblogs": "Fjal lyft frá @{name}",
|
"account.hide_reblogs": "Fjal lyft frá @{name}",
|
||||||
@ -61,6 +63,7 @@
|
|||||||
"account.requested_follow": "{name} hevur biðið um at fylgja tær",
|
"account.requested_follow": "{name} hevur biðið um at fylgja tær",
|
||||||
"account.share": "Deil vanga @{name}'s",
|
"account.share": "Deil vanga @{name}'s",
|
||||||
"account.show_reblogs": "Vís lyft frá @{name}",
|
"account.show_reblogs": "Vís lyft frá @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} postur} other {{counter} postar}}",
|
||||||
"account.unblock": "Banna ikki @{name}",
|
"account.unblock": "Banna ikki @{name}",
|
||||||
"account.unblock_domain": "Banna ikki økisnavnið {domain}",
|
"account.unblock_domain": "Banna ikki økisnavnið {domain}",
|
||||||
"account.unblock_short": "Banna ikki",
|
"account.unblock_short": "Banna ikki",
|
||||||
|
@ -35,7 +35,9 @@
|
|||||||
"account.follow_back": "맞팔로우 하기",
|
"account.follow_back": "맞팔로우 하기",
|
||||||
"account.followers": "팔로워",
|
"account.followers": "팔로워",
|
||||||
"account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.",
|
"account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.",
|
||||||
|
"account.followers_counter": "{count, plural, other {{counter} 팔로워}}",
|
||||||
"account.following": "팔로잉",
|
"account.following": "팔로잉",
|
||||||
|
"account.following_counter": "{count, plural, other {{counter} 팔로잉}}",
|
||||||
"account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.",
|
"account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.",
|
||||||
"account.go_to_profile": "프로필로 이동",
|
"account.go_to_profile": "프로필로 이동",
|
||||||
"account.hide_reblogs": "@{name}의 부스트를 숨기기",
|
"account.hide_reblogs": "@{name}의 부스트를 숨기기",
|
||||||
@ -61,6 +63,7 @@
|
|||||||
"account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다",
|
"account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다",
|
||||||
"account.share": "@{name}의 프로필 공유",
|
"account.share": "@{name}의 프로필 공유",
|
||||||
"account.show_reblogs": "@{name}의 부스트 보기",
|
"account.show_reblogs": "@{name}의 부스트 보기",
|
||||||
|
"account.statuses_counter": "{count, plural, other {{counter} 게시물}}",
|
||||||
"account.unblock": "차단 해제",
|
"account.unblock": "차단 해제",
|
||||||
"account.unblock_domain": "도메인 {domain} 차단 해제",
|
"account.unblock_domain": "도메인 {domain} 차단 해제",
|
||||||
"account.unblock_short": "차단 해제",
|
"account.unblock_short": "차단 해제",
|
||||||
|
@ -35,7 +35,9 @@
|
|||||||
"account.follow_back": "ติดตามกลับ",
|
"account.follow_back": "ติดตามกลับ",
|
||||||
"account.followers": "ผู้ติดตาม",
|
"account.followers": "ผู้ติดตาม",
|
||||||
"account.followers.empty": "ยังไม่มีใครติดตามผู้ใช้นี้",
|
"account.followers.empty": "ยังไม่มีใครติดตามผู้ใช้นี้",
|
||||||
|
"account.followers_counter": "{count, plural, other {{counter} ผู้ติดตาม}}",
|
||||||
"account.following": "กำลังติดตาม",
|
"account.following": "กำลังติดตาม",
|
||||||
|
"account.following_counter": "{count, plural, other {{counter} กำลังติดตาม}}",
|
||||||
"account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร",
|
"account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร",
|
||||||
"account.go_to_profile": "ไปยังโปรไฟล์",
|
"account.go_to_profile": "ไปยังโปรไฟล์",
|
||||||
"account.hide_reblogs": "ซ่อนการดันจาก @{name}",
|
"account.hide_reblogs": "ซ่อนการดันจาก @{name}",
|
||||||
@ -61,6 +63,7 @@
|
|||||||
"account.requested_follow": "{name} ได้ขอติดตามคุณ",
|
"account.requested_follow": "{name} ได้ขอติดตามคุณ",
|
||||||
"account.share": "แชร์โปรไฟล์ของ @{name}",
|
"account.share": "แชร์โปรไฟล์ของ @{name}",
|
||||||
"account.show_reblogs": "แสดงการดันจาก @{name}",
|
"account.show_reblogs": "แสดงการดันจาก @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, other {{counter} โพสต์}}",
|
||||||
"account.unblock": "เลิกปิดกั้น @{name}",
|
"account.unblock": "เลิกปิดกั้น @{name}",
|
||||||
"account.unblock_domain": "เลิกปิดกั้นโดเมน {domain}",
|
"account.unblock_domain": "เลิกปิดกั้นโดเมน {domain}",
|
||||||
"account.unblock_short": "เลิกปิดกั้น",
|
"account.unblock_short": "เลิกปิดกั้น",
|
||||||
|
@ -35,7 +35,9 @@
|
|||||||
"account.follow_back": "Theo dõi lại",
|
"account.follow_back": "Theo dõi lại",
|
||||||
"account.followers": "Người theo dõi",
|
"account.followers": "Người theo dõi",
|
||||||
"account.followers.empty": "Chưa có người theo dõi nào.",
|
"account.followers.empty": "Chưa có người theo dõi nào.",
|
||||||
|
"account.followers_counter": "{count, plural, other {{counter} người theo dõi}}",
|
||||||
"account.following": "Đang theo dõi",
|
"account.following": "Đang theo dõi",
|
||||||
|
"account.following_counter": "{count, plural, other {{counter} đang theo dõi}}",
|
||||||
"account.follows.empty": "Người này chưa theo dõi ai.",
|
"account.follows.empty": "Người này chưa theo dõi ai.",
|
||||||
"account.go_to_profile": "Xem hồ sơ",
|
"account.go_to_profile": "Xem hồ sơ",
|
||||||
"account.hide_reblogs": "Ẩn tút @{name} đăng lại",
|
"account.hide_reblogs": "Ẩn tút @{name} đăng lại",
|
||||||
@ -61,6 +63,7 @@
|
|||||||
"account.requested_follow": "{name} yêu cầu theo dõi bạn",
|
"account.requested_follow": "{name} yêu cầu theo dõi bạn",
|
||||||
"account.share": "Chia sẻ @{name}",
|
"account.share": "Chia sẻ @{name}",
|
||||||
"account.show_reblogs": "Hiện tút do @{name} đăng lại",
|
"account.show_reblogs": "Hiện tút do @{name} đăng lại",
|
||||||
|
"account.statuses_counter": "{count, plural, other {{counter} tút}}",
|
||||||
"account.unblock": "Bỏ chặn @{name}",
|
"account.unblock": "Bỏ chặn @{name}",
|
||||||
"account.unblock_domain": "Bỏ ẩn {domain}",
|
"account.unblock_domain": "Bỏ ẩn {domain}",
|
||||||
"account.unblock_short": "Bỏ chặn",
|
"account.unblock_short": "Bỏ chặn",
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiPreviewCardJSON } from 'mastodon/api_types/statuses';
|
||||||
|
|
||||||
export type { StatusVisibility } from 'mastodon/api_types/statuses';
|
export type { StatusVisibility } from 'mastodon/api_types/statuses';
|
||||||
|
|
||||||
// Temporary until we type it correctly
|
// Temporary until we type it correctly
|
||||||
export type Status = Immutable.Map<string, unknown>;
|
export type Status = Immutable.Map<string, unknown>;
|
||||||
|
|
||||||
|
type CardShape = Required<ApiPreviewCardJSON>;
|
||||||
|
|
||||||
|
export type Card = RecordOf<CardShape>;
|
||||||
|
@ -10468,12 +10468,14 @@ noscript {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.verified {
|
&.verified {
|
||||||
dd {
|
dd {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -291,6 +291,7 @@ cs:
|
|||||||
update_custom_emoji_html: Uživatel %{name} aktualizoval emoji %{target}
|
update_custom_emoji_html: Uživatel %{name} aktualizoval emoji %{target}
|
||||||
update_domain_block_html: "%{name} aktualizoval blokaci domény %{target}"
|
update_domain_block_html: "%{name} aktualizoval blokaci domény %{target}"
|
||||||
update_ip_block_html: "%{name} změnil pravidlo pro IP %{target}"
|
update_ip_block_html: "%{name} změnil pravidlo pro IP %{target}"
|
||||||
|
update_report_html: "%{name} aktualizoval hlášení %{target}"
|
||||||
update_status_html: Uživatel %{name} aktualizoval příspěvek uživatele %{target}
|
update_status_html: Uživatel %{name} aktualizoval příspěvek uživatele %{target}
|
||||||
update_user_role_html: "%{name} změnil %{target} roli"
|
update_user_role_html: "%{name} změnil %{target} roli"
|
||||||
deleted_account: smazaný účet
|
deleted_account: smazaný účet
|
||||||
@ -298,6 +299,7 @@ cs:
|
|||||||
filter_by_action: Filtrovat podle akce
|
filter_by_action: Filtrovat podle akce
|
||||||
filter_by_user: Filtrovat podle uživatele
|
filter_by_user: Filtrovat podle uživatele
|
||||||
title: Protokol auditu
|
title: Protokol auditu
|
||||||
|
unavailable_instance: "(doména není k dispozici)"
|
||||||
announcements:
|
announcements:
|
||||||
destroyed_msg: Oznámení bylo úspěšně odstraněno!
|
destroyed_msg: Oznámení bylo úspěšně odstraněno!
|
||||||
edit:
|
edit:
|
||||||
@ -984,6 +986,7 @@ cs:
|
|||||||
delete: Smazat
|
delete: Smazat
|
||||||
edit_preset: Upravit předlohu pro varování
|
edit_preset: Upravit předlohu pro varování
|
||||||
empty: Zatím jste nedefinovali žádné předlohy varování.
|
empty: Zatím jste nedefinovali žádné předlohy varování.
|
||||||
|
title: Předvolby varování
|
||||||
webhooks:
|
webhooks:
|
||||||
add_new: Přidat koncový bod
|
add_new: Přidat koncový bod
|
||||||
delete: Smazat
|
delete: Smazat
|
||||||
|
@ -166,6 +166,7 @@ cs:
|
|||||||
admin:write:reports: provádět moderátorské akce s hlášeními
|
admin:write:reports: provádět moderátorské akce s hlášeními
|
||||||
crypto: používat end-to-end šifrování
|
crypto: používat end-to-end šifrování
|
||||||
follow: upravovat vztahy mezi profily
|
follow: upravovat vztahy mezi profily
|
||||||
|
profile: číst pouze základní informace o vašem účtu
|
||||||
push: přijímat vaše push oznámení
|
push: přijímat vaše push oznámení
|
||||||
read: vidět všechna data vašeho účtu
|
read: vidět všechna data vašeho účtu
|
||||||
read:accounts: vidět informace o účtech
|
read:accounts: vidět informace o účtech
|
||||||
|
@ -28,6 +28,7 @@ Rails.application.routes.draw do
|
|||||||
/public/remote
|
/public/remote
|
||||||
/conversations
|
/conversations
|
||||||
/lists/(*any)
|
/lists/(*any)
|
||||||
|
/links/(*any)
|
||||||
/notifications/(*any)
|
/notifications/(*any)
|
||||||
/favourites
|
/favourites
|
||||||
/bookmarks
|
/bookmarks
|
||||||
|
@ -181,8 +181,8 @@
|
|||||||
"eslint-plugin-formatjs": "^4.10.1",
|
"eslint-plugin-formatjs": "^4.10.1",
|
||||||
"eslint-plugin-import": "~2.29.0",
|
"eslint-plugin-import": "~2.29.0",
|
||||||
"eslint-plugin-jsdoc": "^48.0.0",
|
"eslint-plugin-jsdoc": "^48.0.0",
|
||||||
"eslint-plugin-jsx-a11y": "~6.8.0",
|
"eslint-plugin-jsx-a11y": "~6.9.0",
|
||||||
"eslint-plugin-promise": "~6.2.0",
|
"eslint-plugin-promise": "~6.4.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
|
Loading…
Reference in New Issue
Block a user