Merge remote-tracking branch 'origin/develop' into sensitive-video-fix
This commit is contained in:
commit
ea0146400c
Binary file not shown.
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="icomoon" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="spinster" d="M512 857.6c-226.216 0-409.6-183.384-409.6-409.6v0c0-226.216 183.384-409.6 409.6-409.6v0c226.216 0 409.6 183.384 409.6 409.6v0c0 226.216-183.384 409.6-409.6 409.6v0zM525.84 706.224c32.633 0 64.793-3.777 96.48-11.344 31.687-7.095 59.576-17.748 83.696-31.936l-43.264-104.272c-47.294 25.539-93.176 38.304-137.632 38.304-27.903 0-48.239-4.255-61.008-12.768-12.769-8.040-19.152-18.677-19.152-31.92s7.57-23.171 22.704-29.792c15.134-6.621 39.493-13.482 73.072-20.576 37.835-8.040 69.039-16.797 93.632-26.256 25.066-8.986 46.588-23.648 64.56-43.984 18.445-19.863 27.664-47.059 27.664-81.584 0-29.795-8.279-56.744-24.832-80.864s-41.374-43.515-74.48-58.176c-33.106-14.188-73.314-21.28-120.608-21.28-40.2 0-79.205 4.964-117.040 14.896s-68.577 23.175-92.224 39.728l46.112 103.568c22.228-14.661 48.006-26.486 77.328-35.472s58.168-13.472 86.544-13.472c53.915 0 80.864 13.475 80.864 40.432 0 14.188-7.801 24.595-23.408 31.216-15.134 7.094-39.724 14.432-73.776 22-37.362 8.040-68.582 16.551-93.648 25.536-25.066 9.459-46.572 24.352-64.544 44.688s-26.96 47.763-26.96 82.288c0 30.268 8.279 57.464 24.832 81.584 16.553 24.592 41.143 43.988 73.776 58.176 33.106 14.188 73.545 21.28 121.312 21.28z" />
|
||||
<glyph unicode="" glyph-name="fediverse" d="M553.99 908.789c-46.369-0.785-83.969-37.261-86.545-83.096l-0.010-0.231c-0.083-1.432-0.13-3.108-0.13-4.794 0-46.987 36.77-85.385 83.105-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.431 0.13 3.106 0.13 4.791 0 46.988-36.771 85.387-83.108 87.99l-0.231 0.010c-1.441 0.084-3.127 0.132-4.823 0.132-0.497 0-0.993-0.004-1.487-0.012l0.075 0.001zM459.882 805.031l-251.29-127.347c13.547-13.809 23-31.679 26.366-51.617l0.080-0.57 251.287 127.353c-13.545 13.808-22.997 31.675-26.363 51.611l-0.080 0.57zM641.318 775.903c-9.415-17.78-23.636-31.938-40.939-41.021l-0.532-0.254 198.787-199.554c9.415 17.78 23.634 31.938 40.936 41.021l0.532 0.254zM487.306 751.83l-147.023-287.024 43.408-43.576 155.667 303.891c-20.483 3.55-38.302 13.087-52.060 26.716l0.007-0.007zM599.388 734.397c-12.846-6.718-28.060-10.66-44.195-10.66-1.77 0-3.529 0.047-5.276 0.141l0.244-0.010c-3.232 0.199-6.15 0.516-9.026 0.959l0.542-0.069 22.259-142.535 60.737-9.746zM138.038 697.983c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.43 0.083-3.103 0.13-4.787 0.13-0.51 0-1.018-0.004-1.526-0.013l0.076 0.001zM235.216 624.428c0.752-4.537 1.182-9.766 1.182-15.095 0-1.667-0.042-3.325-0.125-4.972l0.009 0.231c-0.796-13.969-4.43-26.918-10.33-38.52l0.254 0.551 142.645-22.911 28.036 54.751zM479.695 585.167l-28.039-54.757 337.040-54.13c-0.697 4.368-1.096 9.405-1.096 14.535 0 1.678 0.043 3.346 0.127 5.002l-0.009-0.232c0.815 14.158 4.546 27.272 10.597 38.992l-0.254-0.542zM883.076 578.43c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.119 0.131-4.812 0.131-0.501 0-1-0.004-1.499-0.012l0.075 0.001zM225.366 565.098c-9.414-17.777-23.632-31.933-40.931-41.016l-0.532-0.254 227.623-228.511 54.877 27.811zM182.639 523.19c-12.642-6.466-27.577-10.256-43.397-10.256-1.77 0-3.529 0.047-5.276 0.141l0.244-0.010c-3.521 0.199-6.741 0.548-9.909 1.050l0.551-0.072 43.485-278.147c12.642 6.466 27.577 10.256 43.397 10.256 1.77 0 3.529-0.047 5.276-0.141l-0.244 0.010c3.519-0.2 6.737-0.548 9.903-1.050l-0.55 0.072zM576.873 499.359l52.629-336.996c12.457 6.245 27.143 9.902 42.682 9.902 1.773 0 3.535-0.048 5.285-0.142l-0.244 0.010c3.8-0.219 7.286-0.616 10.711-1.192l-0.569 0.079-49.754 318.595zM788.965 474.681l-128.865-65.308 9.501-60.776 145.806 73.896c-13.546 13.809-22.998 31.679-26.363 51.617l-0.080 0.57zM816.386 421.477l-128.362-250.594c20.486-3.55 38.307-13.087 52.065-26.719l-0.007 0.007 128.359 250.591c-20.485 3.551-38.305 13.090-52.062 26.722l0.007-0.007zM302.044 390.153l-74.471-145.382c20.481-3.55 38.298-13.086 52.054-26.714l-0.007 0.007 65.83 128.515zM585.292 371.462l-304.691-154.416c13.549-13.81 23.003-31.682 26.368-51.622l0.080-0.57 287.744 145.83zM525.607 263.696l-54.877-27.811 115.337-115.788c9.415 17.78 23.636 31.938 40.939 41.021l0.532 0.254zM210.049 237.339c-46.369-0.785-83.969-37.261-86.545-83.096l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.12 0.132-4.813 0.132-0.501 0-1.002-0.004-1.501-0.013l0.075 0.001zM307.279 163.476c0.72-4.438 1.131-9.554 1.131-14.766 0-1.675-0.042-3.34-0.126-4.993l0.009 0.232c-0.806-14.078-4.495-27.122-10.481-38.793l0.254 0.546 278.1-44.626c-0.721 4.442-1.133 9.563-1.133 14.779 0 1.671 0.042 3.332 0.126 4.983l-0.009-0.232c0.807 14.078 4.497 27.12 10.484 38.79l-0.254-0.546zM670.509 163.451c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.119 0.131-4.812 0.131-0.501 0-1-0.004-1.499-0.012l0.075 0.001z" />
|
||||
</font></defs></svg>
|
Before Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
Binary file not shown.
|
@ -1,77 +1,77 @@
|
|||
.status-content p {
|
||||
[data-markup] p {
|
||||
@apply mb-4 whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
.status-content p:last-child {
|
||||
[data-markup] p:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.status-content a {
|
||||
[data-markup] a {
|
||||
@apply text-primary-600 dark:text-accent-blue hover:underline;
|
||||
}
|
||||
|
||||
.status-content strong {
|
||||
[data-markup] strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.status-content em {
|
||||
[data-markup] em {
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
.status-content ul,
|
||||
.status-content ol {
|
||||
[data-markup] ul,
|
||||
[data-markup] ol {
|
||||
@apply pl-10 mb-4;
|
||||
}
|
||||
|
||||
.status-content ul {
|
||||
[data-markup] ul {
|
||||
@apply list-disc list-outside;
|
||||
}
|
||||
|
||||
.status-content ol {
|
||||
[data-markup] ol {
|
||||
@apply list-decimal list-outside;
|
||||
}
|
||||
|
||||
.status-content blockquote {
|
||||
[data-markup] blockquote {
|
||||
@apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.status-content code {
|
||||
[data-markup] code {
|
||||
@apply cursor-text font-mono;
|
||||
}
|
||||
|
||||
.status-content p > code,
|
||||
.status-content pre {
|
||||
[data-markup] p > code,
|
||||
[data-markup] pre {
|
||||
@apply bg-gray-100 dark:bg-primary-800;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
.status-content p > code {
|
||||
[data-markup] p > code {
|
||||
@apply py-0.5 px-1 rounded-sm;
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
.status-content pre {
|
||||
[data-markup] pre {
|
||||
@apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all;
|
||||
}
|
||||
|
||||
.status-content pre:last-child {
|
||||
[data-markup] pre:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
/* Markdown images */
|
||||
.status-content img:not(.emojione):not([width][height]) {
|
||||
[data-markup] img:not(.emojione):not([width][height]) {
|
||||
@apply w-full h-72 object-contain rounded-lg overflow-hidden my-4 block;
|
||||
}
|
||||
|
||||
/* User setting to underline links */
|
||||
body.underline-links .status-content a {
|
||||
body.underline-links [data-markup] a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
.status-content .big-emoji img.emojione {
|
||||
[data-markup] .big-emoji img.emojione {
|
||||
@apply inline w-9 h-9 p-1;
|
||||
}
|
||||
|
||||
.status-content .status-link {
|
||||
[data-markup] .status-link {
|
||||
@apply hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import Text, { IText } from './ui/text/text';
|
||||
import './markup.css';
|
||||
|
||||
interface IMarkup extends IText {
|
||||
}
|
||||
|
||||
/** Styles HTML markup returned by the API, such as in account bios and statuses. */
|
||||
const Markup = React.forwardRef<any, IMarkup>((props, ref) => {
|
||||
return (
|
||||
<Text ref={ref} {...props} data-markup />
|
||||
);
|
||||
});
|
||||
|
||||
export default Markup;
|
|
@ -160,16 +160,22 @@ const Item: React.FC<IItem> = ({
|
|||
</div>
|
||||
);
|
||||
} else if (attachment.type === 'image') {
|
||||
const letterboxed = shouldLetterbox(attachment);
|
||||
const letterboxed = total === 1 && shouldLetterbox(attachment);
|
||||
|
||||
thumbnail = (
|
||||
<a
|
||||
className={classNames('media-gallery__item-thumbnail', { letterboxed })}
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={attachment.url}
|
||||
onClick={handleClick}
|
||||
target='_blank'
|
||||
>
|
||||
<StillImage src={attachment.url} alt={attachment.description} />
|
||||
<StillImage
|
||||
className='w-full h-full'
|
||||
src={attachment.url}
|
||||
alt={attachment.description}
|
||||
letterboxed={letterboxed}
|
||||
showExt
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else if (attachment.type === 'gifv') {
|
||||
|
|
|
@ -136,7 +136,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
</HStack>
|
||||
) : null}
|
||||
|
||||
{account.source.get('note', '').length > 0 && (
|
||||
{account.note.length > 0 && (
|
||||
<Text size='sm' dangerouslySetInnerHTML={accountBio} />
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import classNames from 'clsx';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
@ -16,6 +15,7 @@ import { initReport } from 'soapbox/actions/reports';
|
|||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||
import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper';
|
||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
|
@ -127,8 +127,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
} else {
|
||||
onOpenUnauthorizedModal('REPLY');
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleShareClick = () => {
|
||||
|
@ -146,18 +144,13 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
} else {
|
||||
onOpenUnauthorizedModal('FAVOURITE');
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(toggleBookmark(status));
|
||||
};
|
||||
|
||||
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (me) {
|
||||
const modalReblog = () => dispatch(toggleReblog(status));
|
||||
const boostModal = settings.get('boostModal');
|
||||
|
@ -172,8 +165,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const handleQuoteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (me) {
|
||||
dispatch(quoteCompose(status));
|
||||
} else {
|
||||
|
@ -199,12 +190,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const handleDeleteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
doDeleteStatus();
|
||||
};
|
||||
|
||||
const handleRedraftClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
doDeleteStatus(true);
|
||||
};
|
||||
|
||||
|
@ -213,35 +202,29 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const handlePinClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(togglePin(status));
|
||||
};
|
||||
|
||||
const handleMentionClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(mentionCompose(status.account as Account));
|
||||
};
|
||||
|
||||
const handleDirectClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(directCompose(status.account as Account));
|
||||
};
|
||||
|
||||
const handleChatClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
const account = status.account as Account;
|
||||
dispatch(launchChat(account.id, history));
|
||||
};
|
||||
|
||||
const handleMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(initMuteModal(status.account as Account));
|
||||
};
|
||||
|
||||
const handleBlockClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const account = status.get('account') as Account;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/ban.svg'),
|
||||
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.get('acct') }} />,
|
||||
|
@ -257,7 +240,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const handleOpen: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}`);
|
||||
};
|
||||
|
||||
|
@ -269,12 +251,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const handleReport: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(initReport(status.account as Account, status));
|
||||
};
|
||||
|
||||
const handleConversationMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(toggleMuteStatus(status));
|
||||
};
|
||||
|
||||
|
@ -282,8 +262,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const { uri } = status;
|
||||
const textarea = document.createElement('textarea');
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
textarea.textContent = uri;
|
||||
textarea.style.position = 'fixed';
|
||||
|
||||
|
@ -300,18 +278,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const onModerate: React.MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
const account = status.account as Account;
|
||||
dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id }));
|
||||
};
|
||||
|
||||
const handleDeleteStatus: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(deleteStatusModal(intl, status.id));
|
||||
};
|
||||
|
||||
const handleToggleStatusSensitivity: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive));
|
||||
};
|
||||
|
||||
|
@ -550,12 +525,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const canShare = ('share' in navigator) && status.visibility === 'public';
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid='status-action-bar'
|
||||
className={classNames('flex flex-row', {
|
||||
'justify-between': space === 'expand',
|
||||
'space-x-2': space === 'compact',
|
||||
})}
|
||||
<HStack data-testid='status-action-bar'>
|
||||
<HStack
|
||||
justifyContent={space === 'expand' ? 'between' : undefined}
|
||||
space={space === 'compact' ? 2 : undefined}
|
||||
grow={space === 'expand'}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<StatusActionButton
|
||||
title={replyTitle}
|
||||
|
@ -617,7 +592,8 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
icon={require('@tabler/icons/dots.svg')}
|
||||
/>
|
||||
</DropdownMenuContainer>
|
||||
</div>
|
||||
</HStack>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,19 +10,14 @@ import { onlyEmoji as isOnlyEmoji } from 'soapbox/utils/rich-content';
|
|||
|
||||
import { isRtl } from '../rtl';
|
||||
|
||||
import Markup from './markup';
|
||||
import Poll from './polls/poll';
|
||||
import './status-content.css';
|
||||
|
||||
import type { Status, Mention } from 'soapbox/types/entities';
|
||||
|
||||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
||||
const BIG_EMOJI_LIMIT = 10;
|
||||
|
||||
type Point = [
|
||||
x: number,
|
||||
y: number,
|
||||
]
|
||||
|
||||
interface IReadMoreButton {
|
||||
onClick: React.MouseEventHandler,
|
||||
}
|
||||
|
@ -49,7 +44,6 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [onlyEmoji, setOnlyEmoji] = useState(false);
|
||||
|
||||
const startXY = useRef<Point>();
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { greentext } = useSoapboxConfig();
|
||||
|
@ -131,29 +125,6 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
updateStatusLinks();
|
||||
});
|
||||
|
||||
const handleMouseDown: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
startXY.current = [e.clientX, e.clientY];
|
||||
};
|
||||
|
||||
const handleMouseUp: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
if (!startXY.current) return;
|
||||
const target = e.target as HTMLElement;
|
||||
const parentNode = target.parentNode as HTMLElement;
|
||||
|
||||
const [startX, startY] = startXY.current;
|
||||
const [deltaX, deltaY] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
|
||||
|
||||
if (target.localName === 'button' || target.localName === 'a' || (parentNode && (parentNode.localName === 'button' || parentNode.localName === 'a'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (deltaX + deltaY < 5 && e.button === 0 && !(e.ctrlKey || e.metaKey) && onClick) {
|
||||
onClick();
|
||||
}
|
||||
|
||||
startXY.current = undefined;
|
||||
};
|
||||
|
||||
const parsedHtml = useMemo((): string => {
|
||||
const html = translatable && status.translation ? status.translation.get('content')! : status.contentHtml;
|
||||
|
||||
|
@ -173,30 +144,24 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none';
|
||||
|
||||
const content = { __html: parsedHtml };
|
||||
const directionStyle: React.CSSProperties = { direction: 'ltr' };
|
||||
const className = classNames(baseClassName, 'status-content', {
|
||||
const direction = isRtl(status.search_index) ? 'rtl' : 'ltr';
|
||||
const className = classNames(baseClassName, {
|
||||
'cursor-pointer': onClick,
|
||||
'whitespace-normal': withSpoiler,
|
||||
'max-h-[300px]': collapsed,
|
||||
'leading-normal big-emoji': onlyEmoji,
|
||||
});
|
||||
|
||||
if (isRtl(status.search_index)) {
|
||||
directionStyle.direction = 'rtl';
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
const output = [
|
||||
<div
|
||||
<Markup
|
||||
ref={node}
|
||||
tabIndex={0}
|
||||
key='content'
|
||||
className={className}
|
||||
style={directionStyle}
|
||||
direction={direction}
|
||||
dangerouslySetInnerHTML={content}
|
||||
lang={status.language || undefined}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
/>,
|
||||
];
|
||||
|
||||
|
@ -212,14 +177,14 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
return <div className={classNames({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</div>;
|
||||
} else {
|
||||
const output = [
|
||||
<div
|
||||
<Markup
|
||||
ref={node}
|
||||
tabIndex={0}
|
||||
key='content'
|
||||
className={classNames(baseClassName, 'status-content', {
|
||||
className={classNames(baseClassName, {
|
||||
'leading-normal big-emoji': onlyEmoji,
|
||||
})}
|
||||
style={directionStyle}
|
||||
direction={direction}
|
||||
dangerouslySetInnerHTML={content}
|
||||
lang={status.language || undefined}
|
||||
/>,
|
||||
|
|
|
@ -167,7 +167,16 @@ const StatusMedia: React.FC<IStatusMedia> = ({
|
|||
);
|
||||
}
|
||||
|
||||
return media;
|
||||
if (media) {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
{media}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default StatusMedia;
|
||||
|
|
|
@ -110,6 +110,11 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
const handleClick = (e?: React.MouseEvent): void => {
|
||||
e?.stopPropagation();
|
||||
|
||||
// If the user is selecting text, don't focus the status.
|
||||
if (getSelection()?.toString().length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e || !(e.ctrlKey || e.metaKey)) {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
|
|
|
@ -12,10 +12,14 @@ interface IStillImage {
|
|||
src: string,
|
||||
/** Extra CSS styles on the outer <div> element. */
|
||||
style?: React.CSSProperties,
|
||||
/** Whether to display the image contained vs filled in its container. */
|
||||
letterboxed?: boolean,
|
||||
/** Whether to show the file extension in the corner. */
|
||||
showExt?: boolean,
|
||||
}
|
||||
|
||||
/** Renders images on a canvas, only playing GIFs if autoPlayGif is enabled. */
|
||||
const StillImage: React.FC<IStillImage> = ({ alt, className, src, style }) => {
|
||||
const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterboxed = false, showExt = false }) => {
|
||||
const settings = useSettings();
|
||||
const autoPlayGif = settings.get('autoPlayGif');
|
||||
|
||||
|
@ -34,10 +38,56 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style }) => {
|
|||
}
|
||||
};
|
||||
|
||||
/** ClassNames shared between the `<img>` and `<canvas>` elements. */
|
||||
const baseClassName = classNames('w-full h-full block', {
|
||||
'object-contain': letterboxed,
|
||||
'object-cover': !letterboxed,
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-testid='still-image-container' className={classNames(className, 'still-image', { 'still-image--play-on-hover': hoverToPlay })} style={style}>
|
||||
<img src={src} alt={alt} ref={img} onLoad={handleImageLoad} />
|
||||
{hoverToPlay && <canvas ref={canvas} />}
|
||||
<div
|
||||
data-testid='still-image-container'
|
||||
className={classNames(className, 'relative group overflow-hidden isolate')}
|
||||
style={style}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
ref={img}
|
||||
onLoad={handleImageLoad}
|
||||
className={classNames(baseClassName, {
|
||||
'invisible group-hover:visible': hoverToPlay,
|
||||
})}
|
||||
/>
|
||||
|
||||
{hoverToPlay && (
|
||||
<canvas
|
||||
ref={canvas}
|
||||
className={classNames(baseClassName, {
|
||||
'group-hover:invisible': hoverToPlay,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(hoverToPlay && showExt) && (
|
||||
<div className='group-hover:hidden absolute opacity-90 left-2 bottom-2 pointer-events-none'>
|
||||
<ExtensionBadge ext='GIF' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IExtensionBadge {
|
||||
/** File extension. */
|
||||
ext: string,
|
||||
}
|
||||
|
||||
/** Badge displaying a file extension. */
|
||||
const ExtensionBadge: React.FC<IExtensionBadge> = ({ ext }) => {
|
||||
return (
|
||||
<div className='inline-flex items-center px-2 py-0.5 rounded text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100'>
|
||||
{ext}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -33,7 +33,7 @@ const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant = 'def
|
|||
ref={ref}
|
||||
{...filteredProps}
|
||||
className={classNames({
|
||||
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none overflow-hidden': variant === 'rounded',
|
||||
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none overflow-hidden isolate': variant === 'rounded',
|
||||
[sizes[size]]: variant === 'rounded',
|
||||
}, className)}
|
||||
>
|
||||
|
|
|
@ -27,7 +27,7 @@ const spaces = {
|
|||
8: 'space-x-8',
|
||||
};
|
||||
|
||||
interface IHStack {
|
||||
interface IHStack extends Pick<React.HTMLAttributes<HTMLDivElement>, 'onClick'> {
|
||||
/** Vertical alignment of children. */
|
||||
alignItems?: keyof typeof alignItemsOptions
|
||||
/** Extra class names on the <div> element. */
|
||||
|
|
|
@ -54,7 +54,9 @@ export type Sizes = keyof typeof sizes
|
|||
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label'
|
||||
type Directions = 'ltr' | 'rtl'
|
||||
|
||||
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML'> {
|
||||
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML' | 'tabIndex' | 'lang'> {
|
||||
/** Text content. */
|
||||
children?: React.ReactNode,
|
||||
/** How to align the text. */
|
||||
align?: keyof typeof alignments,
|
||||
/** Extra class names for the outer element. */
|
||||
|
@ -84,8 +86,8 @@ interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'danger
|
|||
}
|
||||
|
||||
/** UI-friendly text container with dark mode support. */
|
||||
const Text: React.FC<IText> = React.forwardRef(
|
||||
(props: IText, ref: React.LegacyRef<any>) => {
|
||||
const Text = React.forwardRef<any, IText>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
align,
|
||||
className,
|
||||
|
|
|
@ -74,6 +74,7 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, displayWidth, onOpenMedia
|
|||
src={attachment.preview_url}
|
||||
alt={attachment.description}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
className='w-full h-full rounded-lg overflow-hidden'
|
||||
/>
|
||||
);
|
||||
} else if (['gifv', 'video'].indexOf(attachment.type) !== -1) {
|
||||
|
|
|
@ -551,13 +551,12 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
)}
|
||||
|
||||
<div>
|
||||
<div className='relative h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50'>
|
||||
<div className='relative flex flex-col justify-center h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50 overflow-hidden isolate'>
|
||||
{account.header && (
|
||||
<a href={account.header} onClick={handleHeaderClick} target='_blank'>
|
||||
<StillImage
|
||||
src={account.header}
|
||||
alt='Profile Header'
|
||||
className='absolute inset-0 object-cover md:rounded-t-xl'
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
|
@ -577,7 +576,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
<Avatar
|
||||
src={account.avatar}
|
||||
size={96}
|
||||
className='h-24 w-24 rounded-full ring-4 ring-white dark:ring-primary-900'
|
||||
className='relative h-24 w-24 rounded-full ring-4 ring-white dark:ring-primary-900'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -449,6 +449,7 @@ const Audio: React.FC<IAudio> = (props) => {
|
|||
onMouseLeave={handleMouseLeave}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<audio
|
||||
src={src}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { buildCustomEmojis } from '../../../emoji/emoji';
|
||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../../emoji/emoji';
|
||||
|
||||
import { EmojiPicker } from './emoji-picker-dropdown';
|
||||
import ModifierPicker from './modifier-picker';
|
||||
|
@ -14,19 +14,6 @@ import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
|||
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
const categoriesSort = [
|
||||
'recent',
|
||||
'custom',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'objects',
|
||||
'symbols',
|
||||
'flags',
|
||||
];
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
|
||||
|
@ -71,6 +58,20 @@ const EmojiPickerMenu: React.FC<IEmojiPickerMenu> = ({
|
|||
|
||||
const [modifierOpen, setModifierOpen] = useState(false);
|
||||
|
||||
const categoriesSort = [
|
||||
'recent',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'objects',
|
||||
'symbols',
|
||||
'flags',
|
||||
];
|
||||
|
||||
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(customEmojis) as Set<string>).sort());
|
||||
|
||||
const handleDocumentClick = useCallback(e => {
|
||||
if (node.current && !node.current.contains(e.target)) {
|
||||
onClose();
|
||||
|
|
|
@ -88,5 +88,10 @@ describe('emoji', () => {
|
|||
expect(emojify('💂♀️💂♂️'))
|
||||
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/packs/emoji/1f482-200d-2640-fe0f.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/packs/emoji/1f482-200d-2642-fe0f.svg">');
|
||||
});
|
||||
|
||||
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
|
||||
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'))
|
||||
.toEqual('<p><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg"> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,6 @@ import unicodeMapping from './emoji-unicode-mapping-light';
|
|||
|
||||
const trie = new Trie(Object.keys(unicodeMapping));
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
const emojifyTextNode = (node, customEmojis, autoPlayGif = false) => {
|
||||
let str = node.textContent;
|
||||
|
||||
|
@ -26,7 +24,7 @@ const emojifyTextNode = (node, customEmojis, autoPlayGif = false) => {
|
|||
}
|
||||
}
|
||||
|
||||
let rend, replacement = '';
|
||||
let rend, replacement = null;
|
||||
if (i === str.length) {
|
||||
break;
|
||||
} else if (str[i] === ':') {
|
||||
|
@ -39,7 +37,14 @@ const emojifyTextNode = (node, customEmojis, autoPlayGif = false) => {
|
|||
// if you want additional emoji handler, add statements below which set replacement and return true.
|
||||
if (shortname in customEmojis) {
|
||||
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
|
||||
replacement = `<img draggable="false" class="emojione custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`;
|
||||
replacement = document.createElement('img');
|
||||
replacement.setAttribute('draggable', false);
|
||||
replacement.setAttribute('class', 'emojione custom-emoji');
|
||||
replacement.setAttribute('alt', shortname);
|
||||
replacement.setAttribute('title', shortname);
|
||||
replacement.setAttribute('src', filename);
|
||||
replacement.setAttribute('data-original', customEmojis[shortname].url);
|
||||
replacement.setAttribute('data-static', customEmojis[shortname].static_url);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -47,8 +52,12 @@ const emojifyTextNode = (node, customEmojis, autoPlayGif = false) => {
|
|||
} else { // matched to unicode emoji
|
||||
const { filename, shortCode } = unicodeMapping[match];
|
||||
const title = shortCode ? `:${shortCode}:` : '';
|
||||
const src = joinPublicPath(`packs/emoji/${filename}.svg`);
|
||||
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${src}" />`;
|
||||
replacement = document.createElement('img');
|
||||
replacement.setAttribute('draggable', false);
|
||||
replacement.setAttribute('class', 'emojione');
|
||||
replacement.setAttribute('alt', match);
|
||||
replacement.setAttribute('title', title);
|
||||
replacement.setAttribute('src', joinPublicPath(`packs/emoji/${filename}.svg`));
|
||||
rend = i + match.length;
|
||||
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
||||
if (str.codePointAt(rend) === 65038) {
|
||||
|
@ -58,7 +67,7 @@ const emojifyTextNode = (node, customEmojis, autoPlayGif = false) => {
|
|||
|
||||
fragment.append(document.createTextNode(str.slice(0, i)));
|
||||
if (replacement) {
|
||||
fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]);
|
||||
fragment.append(replacement);
|
||||
}
|
||||
node.textContent = str.slice(0, i);
|
||||
str = str.slice(rend);
|
||||
|
|
|
@ -35,7 +35,7 @@ const SuggestionItem = ({ accountId }: { accountId: string }) => {
|
|||
<HStack alignItems='center' justifyContent='center' space={1}>
|
||||
<Text
|
||||
weight='semibold'
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name }}
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
truncate
|
||||
align='center'
|
||||
size='sm'
|
||||
|
|
|
@ -2,14 +2,13 @@ import * as React from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { prepareRequest } from 'soapbox/actions/consumer-auth';
|
||||
import Markup from 'soapbox/components/markup';
|
||||
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import RegistrationForm from 'soapbox/features/auth-login/components/registration-form';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { capitalize } from 'soapbox/utils/strings';
|
||||
|
||||
import './instance-description.css';
|
||||
|
||||
const LandingPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
|
@ -114,12 +113,10 @@ const LandingPage = () => {
|
|||
{instance.title}
|
||||
</h1>
|
||||
|
||||
<Text size='lg'>
|
||||
<span
|
||||
className='instance-description'
|
||||
<Markup
|
||||
size='lg'
|
||||
dangerouslySetInnerHTML={{ __html: instance.short_description || instance.description }}
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
/* Instance HTML from the API. */
|
||||
.instance-description a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
.instance-description b,
|
||||
.instance-description strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.instance-description i,
|
||||
.instance-description em {
|
||||
@apply italic;
|
||||
}
|
|
@ -2,7 +2,8 @@ import classNames from 'clsx';
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage, FormatDateOptions } from 'react-intl';
|
||||
|
||||
import { Widget, Stack, HStack, Icon, Text } from 'soapbox/components/ui';
|
||||
import Markup from 'soapbox/components/markup';
|
||||
import { Widget, Stack, HStack, Icon } from 'soapbox/components/ui';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
||||
import { CryptoAddress } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
|
@ -51,7 +52,7 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
return (
|
||||
<dl>
|
||||
<dt title={field.name}>
|
||||
<Text weight='bold' tag='span' dangerouslySetInnerHTML={{ __html: field.name_emojified }} />
|
||||
<Markup weight='bold' tag='span' dangerouslySetInnerHTML={{ __html: field.name_emojified }} />
|
||||
</dt>
|
||||
|
||||
<dd
|
||||
|
@ -65,7 +66,7 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
</span>
|
||||
)}
|
||||
|
||||
<Text className='break-words overflow-hidden' tag='span' dangerouslySetInnerHTML={{ __html: field.value_emojified }} />
|
||||
<Markup className='break-words overflow-hidden' tag='span' dangerouslySetInnerHTML={{ __html: field.value_emojified }} />
|
||||
</HStack>
|
||||
</dd>
|
||||
</dl>
|
||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import Markup from 'soapbox/components/markup';
|
||||
import { Icon, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
@ -139,13 +140,6 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
return (
|
||||
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
||||
<Stack space={2}>
|
||||
{/* Not sure if this is actual used. */}
|
||||
{/* <div className='profile-info-panel-content__deactivated'>
|
||||
<FormattedMessage
|
||||
id='account.deactivated_description' defaultMessage='This account has been deactivated.'
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
<Stack>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} />
|
||||
|
@ -178,8 +172,8 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
|
||||
<ProfileStats account={account} />
|
||||
|
||||
{account.note.length > 0 && account.note !== '<p></p>' && (
|
||||
<Text size='sm' dangerouslySetInnerHTML={content} />
|
||||
{account.note.length > 0 && (
|
||||
<Markup size='sm' dangerouslySetInnerHTML={content} />
|
||||
)}
|
||||
|
||||
<div className='flex flex-col md:flex-row items-start md:flex-wrap md:items-center gap-2'>
|
||||
|
|
|
@ -36,13 +36,9 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
|||
<div className='relative'>
|
||||
<Stack space={2}>
|
||||
<Stack>
|
||||
<div className='-mt-4 -mx-4 h-24 bg-gray-200 relative'>
|
||||
<div className='-mt-4 -mx-4 h-24 bg-gray-200 relative overflow-hidden'>
|
||||
{header && (
|
||||
<StillImage
|
||||
src={account.get('header')}
|
||||
className='absolute inset-0 object-cover'
|
||||
alt=''
|
||||
/>
|
||||
<StillImage src={account.header} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -269,6 +269,15 @@ const fixBirthday = (account: ImmutableMap<string, any>) => {
|
|||
return account.set('birthday', birthday || '');
|
||||
};
|
||||
|
||||
/** Rewrite `<p></p>` to empty string. */
|
||||
const fixNote = (account: ImmutableMap<string, any>) => {
|
||||
if (account.get('note') === '<p></p>') {
|
||||
return account.set('note', '');
|
||||
} else {
|
||||
return account;
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeAccount = (account: Record<string, any>) => {
|
||||
return AccountRecord(
|
||||
ImmutableMap(fromJS(account)).withMutations(account => {
|
||||
|
@ -289,6 +298,7 @@ export const normalizeAccount = (account: Record<string, any>) => {
|
|||
fixUsername(account);
|
||||
fixDisplayName(account);
|
||||
fixBirthday(account);
|
||||
fixNote(account);
|
||||
addInternalFields(account);
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import { normalizeStatus } from 'soapbox/normalizers';
|
||||
|
@ -195,6 +195,24 @@ const simulateFavourite = (
|
|||
return state.set(statusId, updatedStatus);
|
||||
};
|
||||
|
||||
interface Translation {
|
||||
content: string,
|
||||
detected_source_language: string,
|
||||
provider: string,
|
||||
}
|
||||
|
||||
/** Import translation from translation service into the store. */
|
||||
const importTranslation = (state: State, statusId: string, translation: Translation) => {
|
||||
const map = ImmutableMap(translation);
|
||||
const result = map.set('content', stripCompatibilityFeatures(map.get('content', '')));
|
||||
return state.setIn([statusId, 'translation'], result);
|
||||
};
|
||||
|
||||
/** Delete translation from the store. */
|
||||
const deleteTranslation = (state: State, statusId: string) => {
|
||||
return state.deleteIn([statusId, 'translation']);
|
||||
};
|
||||
|
||||
const initialState: State = ImmutableMap();
|
||||
|
||||
export default function statuses(state = initialState, action: AnyAction): State {
|
||||
|
@ -258,9 +276,9 @@ export default function statuses(state = initialState, action: AnyAction): State
|
|||
case STATUS_DELETE_FAIL:
|
||||
return incrementReplyCount(state, action.params);
|
||||
case STATUS_TRANSLATE_SUCCESS:
|
||||
return state.setIn([action.id, 'translation'], fromJS(action.translation));
|
||||
return importTranslation(state, action.id, action.translation);
|
||||
case STATUS_TRANSLATE_UNDO:
|
||||
return state.deleteIn([action.id, 'translation']);
|
||||
return deleteTranslation(state, action.id);
|
||||
case TIMELINE_DELETE:
|
||||
return deleteStatus(state, action.id, action.references);
|
||||
default:
|
||||
|
|
|
@ -72,10 +72,6 @@ a .account__avatar {
|
|||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
|
||||
&.still-image {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
@import 'components/columns';
|
||||
@import 'components/search';
|
||||
@import 'components/react-toggle';
|
||||
@import 'components/still-image';
|
||||
@import 'components/video-player';
|
||||
@import 'components/audio-player';
|
||||
@import 'components/profile-hover-card';
|
||||
|
|
|
@ -352,11 +352,6 @@
|
|||
&__preview {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&__item-thumbnail img,
|
||||
&__item-thumbnail .still-image img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-messages__divider {
|
||||
|
|
|
@ -57,34 +57,9 @@
|
|||
line-height: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&,
|
||||
.still-image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
@apply object-cover rounded-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.still-image--play-on-hover::before {
|
||||
content: 'GIF';
|
||||
position: absolute;
|
||||
color: var(--primary-text-color);
|
||||
background: var(--foreground-color);
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -92,17 +67,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.status__wrapper {
|
||||
.media-gallery__item-thumbnail.letterboxed {
|
||||
&,
|
||||
.still-image {
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-gallery__preview {
|
||||
@apply bg-gray-200 dark:bg-gray-900 rounded-lg;
|
||||
width: 100%;
|
||||
|
@ -113,23 +77,6 @@
|
|||
left: 0;
|
||||
z-index: 0;
|
||||
|
||||
.still-image--play-on-hover::before {
|
||||
content: 'GIF';
|
||||
position: absolute;
|
||||
color: var(--primary-text-color);
|
||||
background: var(--foreground-color);
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
.still-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img,
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
&--play-on-hover {
|
||||
img {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:hover canvas {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
}
|
||||
|
||||
.emoji-mart .emoji-mart-emoji {
|
||||
@apply p-1.5;
|
||||
@apply p-1.5 align-middle;
|
||||
}
|
||||
|
||||
.emoji-mart-bar {
|
||||
|
|
|
@ -58,25 +58,3 @@
|
|||
line-height: #{$px + "px"};
|
||||
line-height: #{$rem + "rem"};
|
||||
}
|
||||
|
||||
// Soapbox icon font
|
||||
@font-face {
|
||||
font-family: 'soapbox';
|
||||
src: url('../assets/fonts/soapbox/soapbox.eot?pryg6i');
|
||||
src: url('../assets/fonts/soapbox/soapbox.eot?pryg6i#iefix') format('embedded-opentype'),
|
||||
url('../assets/fonts/soapbox/soapbox.ttf?pryg6i') format('truetype'),
|
||||
url('../assets/fonts/soapbox/soapbox.woff?pryg6i') format('woff'),
|
||||
url('../assets/fonts/soapbox/soapbox.svg?pryg6i#soapbox') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.fa-fediverse::before {
|
||||
font-family: 'soapbox';
|
||||
content: "\e901";
|
||||
}
|
||||
|
||||
.fa-spinster::before {
|
||||
font-family: 'soapbox';
|
||||
content: "\e900";
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue