pl-fe: suspicious link warning
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -4,6 +4,7 @@ import DOMPurify from 'isomorphic-dompurify';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import minBy from 'lodash/minBy';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
@ -20,6 +21,15 @@ import type { CustomEmoji, Mention } from 'pl-api';
|
||||
|
||||
const GREENTEXT_CLASS = 'dark:text-accent-green text-lime-600';
|
||||
|
||||
const checkSuspiciousUrl = (url: string): boolean => {
|
||||
try {
|
||||
const { host } = new URL(url);
|
||||
return /^verify\.form/.test(host);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const nodesToText = (nodes: Array<DOMNode>): string =>
|
||||
nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array<DOMNode>) : '').join('');
|
||||
|
||||
@ -173,6 +183,8 @@ function parseContent({
|
||||
|
||||
const hashtags: Array<string> = [];
|
||||
|
||||
let hasSuspiciousUrl = false;
|
||||
|
||||
const transformText = (data: string, key?: React.Key) => {
|
||||
const text = speakAsCat ? nyaize(data) : data;
|
||||
|
||||
@ -264,6 +276,10 @@ function parseContent({
|
||||
}
|
||||
}
|
||||
|
||||
if (checkSuspiciousUrl(domNode.attribs.href)) {
|
||||
hasSuspiciousUrl = true;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@ -296,7 +312,21 @@ function parseContent({
|
||||
},
|
||||
};
|
||||
|
||||
const content = parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);
|
||||
let content = parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);
|
||||
|
||||
if (hasSuspiciousUrl) {
|
||||
content = (
|
||||
<>
|
||||
<div className='mb-2 rounded border border-solid border-gray-400 px-2.5 py-2 text-xs text-gray-900 dark:border-gray-800 dark:text-white'>
|
||||
<FormattedMessage
|
||||
id='suspicious_url_warning.body'
|
||||
defaultMessage='This post might include a suspicious link. Please be cautious before entering any personal data or payment information.'
|
||||
/>
|
||||
</div>
|
||||
{content}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (extractHashtags) return {
|
||||
content,
|
||||
|
||||
533
packages/pl-fe/src/components/ui/multiselect.tsx.bak
Normal file
533
packages/pl-fe/src/components/ui/multiselect.tsx.bak
Normal file
@ -0,0 +1,533 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
|
||||
import Icon from './icon';
|
||||
|
||||
interface IMultiselectProps {
|
||||
options: any;
|
||||
// disablePreSelectedValues?: boolean;
|
||||
selectedValues?: any;
|
||||
displayValue: string;
|
||||
placeholder?: string;
|
||||
emptyRecordMsg?: string;
|
||||
onSelect?: (selectedList:any, selectedItem: any) => void;
|
||||
onRemove?: (selectedList:any, selectedItem: any) => void;
|
||||
disable?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function useOutsideAlerter(ref, clickEvent) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
clickEvent();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [ref]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that alerts if you click outside of it
|
||||
*/
|
||||
function OutsideAlerter(props) {
|
||||
const wrapperRef = useRef(null);
|
||||
useOutsideAlerter(wrapperRef, props.outsideClick);
|
||||
return <div ref={wrapperRef}>{props.children}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class Multiselect extends React.Component<IMultiselectProps, any> {
|
||||
|
||||
static defaultProps: IMultiselectProps;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(props: IMultiselectProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
inputValue: '',
|
||||
options: props.options,
|
||||
filteredOptions: props.options,
|
||||
unfilteredOptions: props.options,
|
||||
selectedValues: Object.assign([], props.selectedValues),
|
||||
preSelectedValues: Object.assign([], props.selectedValues),
|
||||
toggleOptionsList: false,
|
||||
highlightOption: 0,
|
||||
// keepSearchTerm: props.keepSearchTerm,
|
||||
groupedObject: [],
|
||||
};
|
||||
// @ts-ignore
|
||||
this.optionTimeout = null;
|
||||
// @ts-ignore
|
||||
this.searchWrapper = React.createRef();
|
||||
// @ts-ignore
|
||||
this.searchBox = React.createRef();
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onFocus = this.onFocus.bind(this);
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.renderMultiselectContainer = this.renderMultiselectContainer.bind(this);
|
||||
this.renderSelectedList = this.renderSelectedList.bind(this);
|
||||
this.onRemoveSelectedItem = this.onRemoveSelectedItem.bind(this);
|
||||
this.toggelOptionList = this.toggelOptionList.bind(this);
|
||||
this.onArrowKeyNavigation = this.onArrowKeyNavigation.bind(this);
|
||||
this.onSelectItem = this.onSelectItem.bind(this);
|
||||
this.filterOptionsByInput = this.filterOptionsByInput.bind(this);
|
||||
this.removeSelectedValuesFromOptions = this.removeSelectedValuesFromOptions.bind(this);
|
||||
this.isSelectedValue = this.isSelectedValue.bind(this);
|
||||
this.isDisablePreSelectedValues = this.isDisablePreSelectedValues.bind(this);
|
||||
this.renderNormalOption = this.renderNormalOption.bind(this);
|
||||
this.listenerCallback = this.listenerCallback.bind(this);
|
||||
this.resetSelectedValues = this.resetSelectedValues.bind(this);
|
||||
this.getSelectedItems = this.getSelectedItems.bind(this);
|
||||
this.getSelectedItemsCount = this.getSelectedItemsCount.bind(this);
|
||||
this.hideOnClickOutside = this.hideOnClickOutside.bind(this);
|
||||
this.onCloseOptionList = this.onCloseOptionList.bind(this);
|
||||
this.isVisible = this.isVisible.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
initialSetValue() {
|
||||
this.removeSelectedValuesFromOptions(false);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
resetSelectedValues() {
|
||||
const { unfilteredOptions } = this.state;
|
||||
return new Promise((resolve) => {
|
||||
this.setState({
|
||||
selectedValues: [],
|
||||
preSelectedValues: [],
|
||||
options: unfilteredOptions,
|
||||
filteredOptions: unfilteredOptions,
|
||||
}, () => {
|
||||
// @ts-ignore
|
||||
resolve();
|
||||
this.initialSetValue();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getSelectedItems() {
|
||||
return this.state.selectedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getSelectedItemsCount() {
|
||||
return this.state.selectedValues.length;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.initialSetValue();
|
||||
// @ts-ignore
|
||||
this.searchWrapper.current.addEventListener('click', this.listenerCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
componentDidUpdate(prevProps: IMultiselectProps) {
|
||||
const { options, selectedValues } = this.props;
|
||||
const { options: prevOptions, selectedValues: prevSelectedvalues } = prevProps;
|
||||
if (JSON.stringify(prevOptions) !== JSON.stringify(options)) {
|
||||
this.setState({ options, filteredOptions: options, unfilteredOptions: options }, this.initialSetValue);
|
||||
}
|
||||
if (JSON.stringify(prevSelectedvalues) !== JSON.stringify(selectedValues)) {
|
||||
this.setState({ selectedValues: Object.assign([], selectedValues), preSelectedValues: Object.assign([], selectedValues) }, this.initialSetValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
listenerCallback() {
|
||||
// @ts-ignore
|
||||
this.searchBox.current.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
// @ts-ignore
|
||||
if (this.optionTimeout) {
|
||||
// @ts-ignore
|
||||
clearTimeout(this.optionTimeout);
|
||||
}
|
||||
// @ts-ignore
|
||||
this.searchWrapper.current.removeEventListener('click', this.listenerCallback);
|
||||
}
|
||||
|
||||
// Skipcheck flag - value will be true when the func called from on deselect anything.
|
||||
/**
|
||||
*
|
||||
*/
|
||||
removeSelectedValuesFromOptions(skipCheck: boolean) {
|
||||
const { displayValue } = this.props;
|
||||
const { selectedValues = [], unfilteredOptions } = this.state;
|
||||
if (!selectedValues.length && !skipCheck) {
|
||||
return;
|
||||
}
|
||||
const optionList = unfilteredOptions.filter(item => {
|
||||
return selectedValues.findIndex(
|
||||
v => v[displayValue] === item[displayValue],
|
||||
) === -1
|
||||
? true
|
||||
: false;
|
||||
});
|
||||
this.setState(
|
||||
{ options: optionList, filteredOptions: optionList },
|
||||
this.filterOptionsByInput,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onChange(event) {
|
||||
const { onSearch } = this.props;
|
||||
this.setState(
|
||||
{ inputValue: event.target.value },
|
||||
this.filterOptionsByInput,
|
||||
);
|
||||
if (onSearch) {
|
||||
onSearch(event.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
filterOptionsByInput() {
|
||||
let { options, filteredOptions, inputValue } = this.state;
|
||||
const { displayValue } = this.props;
|
||||
options = filteredOptions.filter(i => this.matchValues(i[displayValue], inputValue));
|
||||
this.setState({ options });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
matchValues(value, search) {
|
||||
if (value.toLowerCase) {
|
||||
return value.toLowerCase().indexOf(search.toLowerCase()) > -1;
|
||||
}
|
||||
return value.toString().indexOf(search) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onArrowKeyNavigation(e) {
|
||||
const {
|
||||
options,
|
||||
highlightOption,
|
||||
toggleOptionsList,
|
||||
inputValue,
|
||||
selectedValues,
|
||||
} = this.state;
|
||||
if (e.keyCode === 8 && !inputValue && selectedValues.length) {
|
||||
this.onRemoveSelectedItem(selectedValues.length - 1);
|
||||
}
|
||||
if (!options.length) {
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 38) {
|
||||
if (highlightOption > 0) {
|
||||
this.setState(previousState => ({
|
||||
highlightOption: previousState.highlightOption - 1,
|
||||
}));
|
||||
} else {
|
||||
this.setState({ highlightOption: options.length - 1 });
|
||||
}
|
||||
} else if (e.keyCode === 40) {
|
||||
if (highlightOption < options.length - 1) {
|
||||
this.setState(previousState => ({
|
||||
highlightOption: previousState.highlightOption + 1,
|
||||
}));
|
||||
} else {
|
||||
this.setState({ highlightOption: 0 });
|
||||
}
|
||||
} else if (e.key === 'Enter' && options.length && toggleOptionsList) {
|
||||
if (highlightOption === -1) {
|
||||
return;
|
||||
}
|
||||
this.onSelectItem(options[highlightOption]);
|
||||
}
|
||||
// TODO: Instead of scrollIntoView need to find better soln for scroll the dropwdown container.
|
||||
// setTimeout(() => {
|
||||
// const element = document.querySelector("ul.optionContainer .highlight");
|
||||
// if (element) {
|
||||
// element.scrollIntoView();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onRemoveSelectedItem(item) {
|
||||
let { selectedValues, index = 0 } = this.state;
|
||||
const { onRemove, displayValue } = this.props;
|
||||
index = selectedValues.findIndex(
|
||||
i => i[displayValue] === item[displayValue],
|
||||
);
|
||||
selectedValues.splice(index, 1);
|
||||
onRemove(selectedValues, item);
|
||||
this.setState({ selectedValues }, () => {
|
||||
this.removeSelectedValuesFromOptions(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onSelectItem(item) {
|
||||
const { selectedValues } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
this.setState({
|
||||
inputValue: '',
|
||||
});
|
||||
if (this.isSelectedValue(item)) {
|
||||
this.onRemoveSelectedItem(item);
|
||||
return;
|
||||
}
|
||||
selectedValues.push(item);
|
||||
onSelect(selectedValues, item);
|
||||
this.setState({ selectedValues }, () => {
|
||||
this.removeSelectedValuesFromOptions(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
isSelectedValue(item) {
|
||||
const { displayValue } = this.props;
|
||||
const { selectedValues } = this.state;
|
||||
return (
|
||||
selectedValues.filter(i => i[displayValue] === item[displayValue])
|
||||
.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
renderOptionList() {
|
||||
const { emptyRecordMsg } = this.props;
|
||||
const { options } = this.state;
|
||||
return (
|
||||
<ul className={'optionContainer'}>
|
||||
{options.length === 0 && <span className={'notFound'}>{emptyRecordMsg}</span>}
|
||||
{this.renderNormalOption()}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
renderNormalOption() {
|
||||
const { displayValue } = this.props;
|
||||
const { highlightOption } = this.state;
|
||||
return this.state.options.map((option, i) => {
|
||||
const isSelected = this.isSelectedValue(option);
|
||||
return (
|
||||
<li
|
||||
key={`option${i}`}
|
||||
className={`option ${isSelected ? 'selected' : ''} ${highlightOption === i ? 'highlightOption highlight' : ''} ${this.isDisablePreSelectedValues(option) ? 'disableSelection' : ''}`}
|
||||
onClick={() => this.onSelectItem(option)}
|
||||
>
|
||||
{option[displayValue]}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
renderSelectedList() {
|
||||
const { displayValue } = this.props;
|
||||
const { selectedValues } = this.state;
|
||||
return selectedValues.map((value, index) => (
|
||||
<span className={`chip} ${this.isDisablePreSelectedValues(value) && 'disableSelection'}`} key={index}>
|
||||
{value[displayValue]}
|
||||
{!this.isDisablePreSelectedValues(value) && (
|
||||
<div
|
||||
role='button'
|
||||
onClick={() => this.onRemoveSelectedItem(value)}
|
||||
>
|
||||
<Icon
|
||||
className='ml-1 size-4 hover:cursor-pointer' src={require('@tabler/icons/outline/circle-x.svg')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
isDisablePreSelectedValues(value) {
|
||||
const { displayValue } = this.props;
|
||||
const { preSelectedValues } = this.state;
|
||||
if (!preSelectedValues.length) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
preSelectedValues.filter(i => i[displayValue] === value[displayValue])
|
||||
.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
toggelOptionList() {
|
||||
this.setState({
|
||||
toggleOptionsList: !this.state.toggleOptionsList,
|
||||
highlightOption: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onCloseOptionList() {
|
||||
this.setState({
|
||||
toggleOptionsList: false,
|
||||
highlightOption: 0,
|
||||
inputValue: '',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onFocus(){
|
||||
if (this.state.toggleOptionsList) {
|
||||
// @ts-ignore
|
||||
clearTimeout(this.optionTimeout);
|
||||
} else {
|
||||
this.toggelOptionList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
onBlur(){
|
||||
this.setState({ inputValue: '' }, this.filterOptionsByInput);
|
||||
// @ts-ignore
|
||||
this.optionTimeout = setTimeout(this.onCloseOptionList, 250);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
isVisible(elem) {
|
||||
return !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
hideOnClickOutside() {
|
||||
const element = document.getElementsByClassName('multiselect-container')[0];
|
||||
const outsideClickListener = event => {
|
||||
if (element && !element.contains(event.target) && this.isVisible(element)) {
|
||||
this.toggelOptionList();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', outsideClickListener);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
renderMultiselectContainer() {
|
||||
const { inputValue, toggleOptionsList, selectedValues } = this.state;
|
||||
const { placeholder, id, name, disable, className } = this.props;
|
||||
return (
|
||||
<div className={`multiselect-container multiSelectContainer ${disable ? 'disable_ms' : ''} ${className || ''}`} id={id || 'multiselectContainerReact'}>
|
||||
<div
|
||||
className={'search-wrapper searchWrapper'}
|
||||
ref={this.searchWrapper}
|
||||
>
|
||||
{this.renderSelectedList()}
|
||||
<input
|
||||
type='text'
|
||||
ref={this.searchBox}
|
||||
className='searchBox'
|
||||
id={`${id || 'search'}_input`}
|
||||
name={`${name || 'search_name'}_input`}
|
||||
onChange={this.onChange}
|
||||
value={inputValue}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
placeholder={(selectedValues.length) ? '' : placeholder}
|
||||
onKeyDown={this.onArrowKeyNavigation}
|
||||
autoComplete='off'
|
||||
disabled={disable}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`optionListContainer ${
|
||||
toggleOptionsList ? 'displayBlock' : 'displayNone'
|
||||
}`}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{this.renderOptionList()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<OutsideAlerter outsideClick={this.onCloseOptionList}>
|
||||
{this.renderMultiselectContainer()}
|
||||
</OutsideAlerter>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Multiselect.defaultProps = {
|
||||
options: [],
|
||||
selectedValues: [],
|
||||
displayValue: 'model',
|
||||
placeholder: 'Select',
|
||||
emptyRecordMsg: 'No Options Available',
|
||||
onSelect: () => {},
|
||||
onRemove: () => {},
|
||||
id: '',
|
||||
name: '',
|
||||
className: '',
|
||||
} as IMultiselectProps;
|
||||
@ -2,7 +2,7 @@
|
||||
"about.also_available": "Available in:",
|
||||
"accordion.collapse": "Collapse",
|
||||
"accordion.expand": "Expand",
|
||||
"account.add_or_remove_from_list": "Add or Remove from lists",
|
||||
"account.add_or_remove_from_list": "Add or remove from lists",
|
||||
"account.avatar.alt": "Avatar",
|
||||
"account.avatar.description": "Avatar description",
|
||||
"account.avatar.with_content": "Avatar for {username}: {alt}",
|
||||
@ -1084,7 +1084,7 @@
|
||||
"lightbox.view_context": "View context",
|
||||
"link_preview.more_from_author": "More from {name}",
|
||||
"list.click_to_add": "Click here to add people",
|
||||
"list_adder.header_title": "Add or Remove from Lists",
|
||||
"list_adder.header_title": "Add or remove from lists",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.delete": "Delete list",
|
||||
@ -1123,6 +1123,7 @@
|
||||
"login_form.divider": "or",
|
||||
"login_form.external": "Sign in from remote instance",
|
||||
"login_form.header": "Sign in",
|
||||
"suspicious_url_warning.body": "This post might include a suspicious link. Please be cautious before entering any personal data or payment information.",
|
||||
"manage_group.blocked_members": "Banned members",
|
||||
"manage_group.confirmation.copy": "Copy link",
|
||||
"manage_group.confirmation.info_1": "As the owner of this group, you can assign staff, delete posts and much more.",
|
||||
|
||||
Reference in New Issue
Block a user