add icon for paid patreon sub
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 510 B After Width: | Height: | Size: 510 B |
|
Before Width: | Height: | Size: 741 B After Width: | Height: | Size: 741 B |
|
Before Width: | Height: | Size: 741 B After Width: | Height: | Size: 741 B |
|
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 500 B |
|
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 393 B |
|
Before Width: | Height: | Size: 531 B After Width: | Height: | Size: 531 B |
|
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 388 B |
@@ -7,6 +7,7 @@ export type User = {
|
||||
block: boolean;
|
||||
verified: boolean;
|
||||
email_subscription?: boolean;
|
||||
paid_sub?: boolean;
|
||||
};
|
||||
|
||||
/** data which is used on user-info page */
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
export const OAUTH_DATA = {
|
||||
facebook: require('./assets/facebook.svg').default as string,
|
||||
twitter: require('./assets/twitter.svg').default as string,
|
||||
patreon: require('./assets/patreon.svg').default as string,
|
||||
google: require('./assets/google.svg').default as string,
|
||||
microsoft: require('./assets/microsoft.svg').default as string,
|
||||
yandex: require('./assets/yandex.svg').default as string,
|
||||
dev: require('./assets/dev.svg').default as string,
|
||||
facebook: require('assets/social/facebook.svg').default as string,
|
||||
twitter: require('assets/social/twitter.svg').default as string,
|
||||
patreon: require('assets/social/patreon.svg').default as string,
|
||||
google: require('assets/social/google.svg').default as string,
|
||||
microsoft: require('assets/social/microsoft.svg').default as string,
|
||||
yandex: require('assets/social/yandex.svg').default as string,
|
||||
dev: require('assets/social/dev.svg').default as string,
|
||||
github: {
|
||||
name: 'GitHub',
|
||||
icons: {
|
||||
light: require('./assets/github-light.svg').default as string,
|
||||
dark: require('./assets/github-dark.svg').default as string,
|
||||
light: require('assets/social/github-light.svg').default as string,
|
||||
dark: require('assets/social/github-dark.svg').default as string,
|
||||
},
|
||||
},
|
||||
telegram: require('./assets/telegram.svg').default as string,
|
||||
telegram: require('assets/social/telegram.svg').default as string,
|
||||
} as const;
|
||||
|
||||
export const OAUTH_PROVIDERS = Object.keys(OAUTH_DATA);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user > * + * {
|
||||
margin-left: 4px;
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
import { h } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
jest.mock('react-intl', () => {
|
||||
const messages = require('locales/en.json');
|
||||
const reactIntl = jest.requireActual('react-intl');
|
||||
const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {});
|
||||
|
||||
return {
|
||||
...reactIntl,
|
||||
useIntl: () => intlProvider.state.intl,
|
||||
};
|
||||
});
|
||||
import '@testing-library/jest-dom';
|
||||
import { screen } from '@testing-library/preact';
|
||||
import { render } from 'tests/utils';
|
||||
|
||||
import { useIntl, IntlProvider } from 'react-intl';
|
||||
|
||||
@@ -21,46 +13,58 @@ import { sleep } from 'utils/sleep';
|
||||
|
||||
import { Comment, CommentProps } from './comment';
|
||||
|
||||
function mountComment(props: CommentProps) {
|
||||
function Wrapper(updateProps: Partial<CommentProps> = {}) {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<IntlProvider locale="en" messages={enMessages}>
|
||||
<Comment {...props} {...updateProps} intl={intl} />
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return mount(<Wrapper />);
|
||||
function CommentWithIntl(props: CommentProps) {
|
||||
return <Comment {...props} intl={useIntl()} />;
|
||||
}
|
||||
|
||||
const DefaultProps: Partial<CommentProps> = {
|
||||
post_info: {
|
||||
read_only: false,
|
||||
} as PostInfo,
|
||||
view: 'main',
|
||||
data: {
|
||||
text: 'test comment',
|
||||
vote: 0,
|
||||
user: {
|
||||
id: 'someone',
|
||||
picture: 'somepicture-url',
|
||||
},
|
||||
time: new Date().toString(),
|
||||
locator: {
|
||||
url: 'somelocatorurl',
|
||||
site: 'remark',
|
||||
},
|
||||
} as CommentType,
|
||||
user: {
|
||||
admin: false,
|
||||
id: 'testuser',
|
||||
picture: 'somepicture-url',
|
||||
} as User,
|
||||
} as CommentProps;
|
||||
// @depricated
|
||||
function mountComment(props: CommentProps) {
|
||||
return mount(
|
||||
<IntlProvider locale="en" messages={enMessages}>
|
||||
<CommentWithIntl {...props} />
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultProps() {
|
||||
return {
|
||||
post_info: {
|
||||
read_only: false,
|
||||
} as PostInfo,
|
||||
view: 'main',
|
||||
data: {
|
||||
text: 'test comment',
|
||||
vote: 0,
|
||||
user: {
|
||||
id: 'someone',
|
||||
picture: 'somepicture-url',
|
||||
} as User,
|
||||
time: new Date().toString(),
|
||||
locator: {
|
||||
url: 'somelocatorurl',
|
||||
site: 'remark',
|
||||
},
|
||||
} as CommentType,
|
||||
user: {
|
||||
admin: false,
|
||||
id: 'testuser',
|
||||
picture: 'somepicture-url',
|
||||
} as User,
|
||||
} as CommentProps;
|
||||
}
|
||||
|
||||
const DefaultProps = getDefaultProps();
|
||||
describe('<Comment />', () => {
|
||||
it('should render patreon subscriber icon', async () => {
|
||||
const props = getDefaultProps() as CommentProps;
|
||||
props.data.user.paid_sub = true;
|
||||
|
||||
render(<CommentWithIntl {...props} />);
|
||||
const patreonSubscriberIcon = await screen.findByAltText('Patreon Paid Subscriber');
|
||||
expect(patreonSubscriberIcon).toBeVisible();
|
||||
expect(patreonSubscriberIcon.tagName).toBe('IMG');
|
||||
});
|
||||
|
||||
describe('voting', () => {
|
||||
it('should be disabled for an anonymous user', () => {
|
||||
const wrapper = mountComment({ ...DefaultProps, user: { id: 'anonymous_1' } } as CommentProps);
|
||||
@@ -243,37 +247,24 @@ describe('<Comment />', () => {
|
||||
expect(controls.hasClass('comment__verification_clickable')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should be editable', () => {
|
||||
it('should be editable', async () => {
|
||||
StaticStore.config.edit_duration = 300;
|
||||
|
||||
const initTime = new Date().toString();
|
||||
const changedTime = new Date(Date.now() + 10 * 1000).toString();
|
||||
const props = {
|
||||
...DefaultProps,
|
||||
user: DefaultProps.user as User,
|
||||
data: {
|
||||
...DefaultProps.data,
|
||||
id: '100',
|
||||
user: DefaultProps.user as User,
|
||||
vote: 1,
|
||||
time: initTime,
|
||||
delete: false,
|
||||
orig: 'test',
|
||||
} as CommentType,
|
||||
repliesCount: 0,
|
||||
} as CommentProps;
|
||||
const component = mountComment(props);
|
||||
const comment = component.find(Comment);
|
||||
const props = getDefaultProps();
|
||||
props.repliesCount = 0;
|
||||
props.user!.id = '100';
|
||||
props.data.user.id = '100';
|
||||
Object.assign(props.data, {
|
||||
id: '101',
|
||||
vote: 1,
|
||||
time: new Date().toString(),
|
||||
delete: false,
|
||||
orig: 'test',
|
||||
});
|
||||
|
||||
expect((comment.state('editDeadline') as Date).getTime()).toBe(
|
||||
new Date(new Date(initTime).getTime() + 300 * 1000).getTime()
|
||||
);
|
||||
|
||||
component.setProps({ data: { ...props.data, time: changedTime } });
|
||||
|
||||
expect((comment.state('editDeadline') as Date).getTime()).toBe(
|
||||
new Date(new Date(changedTime).getTime() + 300 * 1000).getTime()
|
||||
);
|
||||
render(<CommentWithIntl {...props} />);
|
||||
// it can be less than 300 due to test checks time
|
||||
expect(['299', '300']).toContain(screen.getByRole('timer').innerText);
|
||||
});
|
||||
|
||||
it('should not be editable', () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getVoteMessage, VoteMessagesTypes } from './getVoteMessage';
|
||||
import { getBlockingDurations } from './getBlockingDurations';
|
||||
import { boundActions } from './connected-comment';
|
||||
|
||||
import style from './comment.module.css';
|
||||
import './styles';
|
||||
|
||||
export type CommentProps = {
|
||||
@@ -79,7 +80,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
/** comment text node. Used in comment text copying */
|
||||
textNode = createRef<HTMLDivElement>();
|
||||
|
||||
updateState = (props: CommentProps) => {
|
||||
updateState(props: CommentProps) {
|
||||
const newState: Partial<State> = {
|
||||
scoreDelta: props.data.vote,
|
||||
cachedScore: props.data.score,
|
||||
@@ -103,7 +104,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
}
|
||||
|
||||
return newState;
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
renderDummy: typeof this.props.inView === 'boolean' ? !this.props.inView : false,
|
||||
@@ -595,26 +596,35 @@ export class Comment extends Component<CommentProps, State> {
|
||||
<Avatar url={o.user.picture} />
|
||||
</div>
|
||||
)}
|
||||
{props.view !== 'user' && (
|
||||
<button onClick={() => this.toggleUserInfoVisibility()} className="comment__username">
|
||||
{o.user.name}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isAdmin && props.view !== 'user' && (
|
||||
<span
|
||||
{...getHandleClickProps(this.toggleVerify)}
|
||||
aria-label={intl.formatMessage(messages.toggleVerification)}
|
||||
title={intl.formatMessage(o.user.verified ? messages.verifiedUser : messages.unverifiedUser)}
|
||||
className={b('comment__verification', {}, { active: o.user.verified, clickable: true })}
|
||||
/>
|
||||
)}
|
||||
{!isAdmin && !!o.user.verified && props.view !== 'user' && (
|
||||
<span
|
||||
title={intl.formatMessage(messages.verifiedUser)}
|
||||
className={b('comment__verification', {}, { active: true })}
|
||||
/>
|
||||
)}
|
||||
<div className={style.user}>
|
||||
{props.view !== 'user' && (
|
||||
<button onClick={() => this.toggleUserInfoVisibility()} className="comment__username">
|
||||
{o.user.name}
|
||||
</button>
|
||||
)}
|
||||
{o.user.paid_sub && (
|
||||
<img
|
||||
width={12}
|
||||
height={12}
|
||||
src={require('assets/social/patreon.svg').default}
|
||||
alt={intl.formatMessage(messages.paidPatreon)}
|
||||
/>
|
||||
)}
|
||||
{isAdmin && props.view !== 'user' && (
|
||||
<span
|
||||
{...getHandleClickProps(this.toggleVerify)}
|
||||
aria-label={intl.formatMessage(messages.toggleVerification)}
|
||||
title={intl.formatMessage(o.user.verified ? messages.verifiedUser : messages.unverifiedUser)}
|
||||
className={b('comment__verification', {}, { active: o.user.verified, clickable: true })}
|
||||
/>
|
||||
)}
|
||||
{!isAdmin && !!o.user.verified && props.view !== 'user' && (
|
||||
<span
|
||||
title={intl.formatMessage(messages.verifiedUser)}
|
||||
className={b('comment__verification', {}, { active: true })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__time">
|
||||
{getLocalDatetime(this.props.intl, o.time)}
|
||||
@@ -878,4 +888,8 @@ const messages = defineMessages({
|
||||
id: 'comment.time',
|
||||
defaultMessage: '{day} at {time}',
|
||||
},
|
||||
paidPatreon: {
|
||||
id: 'comment.paid-patreon',
|
||||
defaultMessage: 'Patreon Paid Subscriber',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,6 +56,6 @@ export class Countdown extends Component<Props, State> {
|
||||
}, 1000);
|
||||
}
|
||||
render(props: Props) {
|
||||
return <span {...exclude(props, 'time', 'onTimePassed')} ref={this.elemRef} />;
|
||||
return <span role="timer" {...exclude(props, 'time', 'onTimePassed')} ref={this.elemRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ module.exports = (_, { mode, analyze }) => {
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
publicPath: PUBLIC_PATH,
|
||||
limit: 1200,
|
||||
limit: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||