mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-25 11:01:12 +00:00
Refactor <FollowedTags>
into TypeScript (#34355)
This commit is contained in:
parent
887336f2c6
commit
bdf9baa2e8
|
@ -1,81 +0,0 @@
|
||||||
import api, { getLinks } from '../api';
|
|
||||||
|
|
||||||
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
|
|
||||||
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
|
|
||||||
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
|
|
||||||
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
|
|
||||||
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
|
|
||||||
|
|
||||||
export const fetchFollowedHashtags = () => (dispatch) => {
|
|
||||||
dispatch(fetchFollowedHashtagsRequest());
|
|
||||||
|
|
||||||
api().get('/api/v1/followed_tags').then(response => {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
|
||||||
}).catch(err => {
|
|
||||||
dispatch(fetchFollowedHashtagsFail(err));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function fetchFollowedHashtagsRequest() {
|
|
||||||
return {
|
|
||||||
type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchFollowedHashtagsSuccess(followed_tags, next) {
|
|
||||||
return {
|
|
||||||
type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
|
|
||||||
followed_tags,
|
|
||||||
next,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchFollowedHashtagsFail(error) {
|
|
||||||
return {
|
|
||||||
type: FOLLOWED_HASHTAGS_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expandFollowedHashtags() {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
const url = getState().getIn(['followed_tags', 'next']);
|
|
||||||
|
|
||||||
if (url === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(expandFollowedHashtagsRequest());
|
|
||||||
|
|
||||||
api().get(url).then(response => {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(expandFollowedHashtagsFail(error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expandFollowedHashtagsRequest() {
|
|
||||||
return {
|
|
||||||
type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expandFollowedHashtagsSuccess(followed_tags, next) {
|
|
||||||
return {
|
|
||||||
type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
|
|
||||||
followed_tags,
|
|
||||||
next,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expandFollowedHashtagsFail(error) {
|
|
||||||
return {
|
|
||||||
type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRequestPost, apiRequestGet } from 'mastodon/api';
|
import api, { getLinks, apiRequestPost, apiRequestGet } from 'mastodon/api';
|
||||||
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
|
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
|
||||||
|
|
||||||
export const apiGetTag = (tagId: string) =>
|
export const apiGetTag = (tagId: string) =>
|
||||||
|
@ -9,3 +9,15 @@ export const apiFollowTag = (tagId: string) =>
|
||||||
|
|
||||||
export const apiUnfollowTag = (tagId: string) =>
|
export const apiUnfollowTag = (tagId: string) =>
|
||||||
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`);
|
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`);
|
||||||
|
|
||||||
|
export const apiGetFollowedTags = async (url?: string) => {
|
||||||
|
const response = await api().request<ApiHashtagJSON[]>({
|
||||||
|
method: 'GET',
|
||||||
|
url: url ?? '/api/v1/followed_tags',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags: response.data,
|
||||||
|
links: getLinks(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -106,6 +106,7 @@ export interface HashtagProps {
|
||||||
to: string;
|
to: string;
|
||||||
uses?: number;
|
uses?: number;
|
||||||
withGraph?: boolean;
|
withGraph?: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Hashtag: React.FC<HashtagProps> = ({
|
export const Hashtag: React.FC<HashtagProps> = ({
|
||||||
|
@ -117,6 +118,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
|
||||||
className,
|
className,
|
||||||
description,
|
description,
|
||||||
withGraph = true,
|
withGraph = true,
|
||||||
|
children,
|
||||||
}) => (
|
}) => (
|
||||||
<div className={classNames('trends__item', className)}>
|
<div className={classNames('trends__item', className)}>
|
||||||
<div className='trends__item__name'>
|
<div className='trends__item__name'>
|
||||||
|
@ -158,5 +160,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
|
||||||
</SilentErrorBoundary>
|
</SilentErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{children && <div className='trends__item__buttons'>{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
|
|
||||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
|
||||||
import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
|
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
|
||||||
import { Hashtag } from 'mastodon/components/hashtag';
|
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
hashtags: state.getIn(['followed_tags', 'items']),
|
|
||||||
isLoading: state.getIn(['followed_tags', 'isLoading'], true),
|
|
||||||
hasMore: !!state.getIn(['followed_tags', 'next']),
|
|
||||||
});
|
|
||||||
|
|
||||||
class FollowedTags extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
params: PropTypes.object.isRequired,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
hashtags: ImmutablePropTypes.list,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
hasMore: PropTypes.bool,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatch(fetchFollowedHashtags());
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoadMore = debounce(() => {
|
|
||||||
this.props.dispatch(expandFollowedHashtags());
|
|
||||||
}, 300, { leading: true });
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
|
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column bindToDocument={!multiColumn}>
|
|
||||||
<ColumnHeader
|
|
||||||
icon='hashtag'
|
|
||||||
iconComponent={TagIcon}
|
|
||||||
title={intl.formatMessage(messages.heading)}
|
|
||||||
showBackButton
|
|
||||||
multiColumn={multiColumn}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ScrollableList
|
|
||||||
scrollKey='followed_tags'
|
|
||||||
emptyMessage={emptyMessage}
|
|
||||||
hasMore={hasMore}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onLoadMore={this.handleLoadMore}
|
|
||||||
bindToDocument={!multiColumn}
|
|
||||||
>
|
|
||||||
{hashtags.map((hashtag) => (
|
|
||||||
<Hashtag
|
|
||||||
key={hashtag.get('name')}
|
|
||||||
name={hashtag.get('name')}
|
|
||||||
to={`/tags/${hashtag.get('name')}`}
|
|
||||||
withGraph={false}
|
|
||||||
// Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
|
|
||||||
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
|
||||||
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ScrollableList>
|
|
||||||
|
|
||||||
<Helmet>
|
|
||||||
<meta name='robots' content='noindex' />
|
|
||||||
</Helmet>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(injectIntl(FollowedTags));
|
|
161
app/javascript/mastodon/features/followed_tags/index.tsx
Normal file
161
app/javascript/mastodon/features/followed_tags/index.tsx
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
|
import { unfollowHashtag } from 'mastodon/actions/tags_typed';
|
||||||
|
import { apiGetFollowedTags } from 'mastodon/api/tags';
|
||||||
|
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import { Column } from 'mastodon/components/column';
|
||||||
|
import type { ColumnRef } from 'mastodon/components/column';
|
||||||
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
|
import { Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const FollowedTag: React.FC<{
|
||||||
|
tag: ApiHashtagJSON;
|
||||||
|
onUnfollow: (arg0: string) => void;
|
||||||
|
}> = ({ tag, onUnfollow }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const tagId = tag.name;
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
void dispatch(unfollowHashtag({ tagId })).then((result) => {
|
||||||
|
if (isFulfilled(result)) {
|
||||||
|
onUnfollow(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}, [dispatch, onUnfollow, tagId]);
|
||||||
|
|
||||||
|
const people =
|
||||||
|
parseInt(tag.history[0].accounts) +
|
||||||
|
parseInt(tag.history[1]?.accounts ?? '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Hashtag
|
||||||
|
name={tag.name}
|
||||||
|
to={`/tags/${tag.name}`}
|
||||||
|
withGraph={false}
|
||||||
|
people={people}
|
||||||
|
>
|
||||||
|
<Button onClick={handleClick}>
|
||||||
|
<FormattedMessage id='account.unfollow' defaultMessage='Unfollow' />
|
||||||
|
</Button>
|
||||||
|
</Hashtag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FollowedTags: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [next, setNext] = useState<string | undefined>();
|
||||||
|
const hasMore = !!next;
|
||||||
|
const columnRef = useRef<ColumnRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
void apiGetFollowedTags()
|
||||||
|
.then(({ tags, links }) => {
|
||||||
|
const next = links.refs.find((link) => link.rel === 'next');
|
||||||
|
|
||||||
|
setTags(tags);
|
||||||
|
setLoading(false);
|
||||||
|
setNext(next?.uri);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [setTags, setLoading, setNext]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
void apiGetFollowedTags(next)
|
||||||
|
.then(({ tags, links }) => {
|
||||||
|
const next = links.refs.find((link) => link.rel === 'next');
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
setTags((previousTags) => [...previousTags, ...tags]);
|
||||||
|
setNext(next?.uri);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [setTags, setLoading, setNext, next]);
|
||||||
|
|
||||||
|
const handleUnfollow = useCallback(
|
||||||
|
(tagId: string) => {
|
||||||
|
setTags((tags) => tags.filter((tag) => tag.name !== tagId));
|
||||||
|
},
|
||||||
|
[setTags],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHeaderClick = useCallback(() => {
|
||||||
|
columnRef.current?.scrollTop();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.followed_tags'
|
||||||
|
defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
ref={columnRef}
|
||||||
|
label={intl.formatMessage(messages.heading)}
|
||||||
|
>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='hashtag'
|
||||||
|
iconComponent={TagIcon}
|
||||||
|
title={intl.formatMessage(messages.heading)}
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='followed_tags'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
hasMore={hasMore}
|
||||||
|
isLoading={loading}
|
||||||
|
showLoading={loading && tags.length === 0}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
trackScroll={!multiColumn}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<FollowedTag key={tag.name} tag={tag} onUnfollow={handleUnfollow} />
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.heading)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default FollowedTags;
|
|
@ -1,43 +0,0 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
FOLLOWED_HASHTAGS_FETCH_REQUEST,
|
|
||||||
FOLLOWED_HASHTAGS_FETCH_SUCCESS,
|
|
||||||
FOLLOWED_HASHTAGS_FETCH_FAIL,
|
|
||||||
FOLLOWED_HASHTAGS_EXPAND_REQUEST,
|
|
||||||
FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
|
|
||||||
FOLLOWED_HASHTAGS_EXPAND_FAIL,
|
|
||||||
} from 'mastodon/actions/tags';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
|
||||||
items: ImmutableList(),
|
|
||||||
isLoading: false,
|
|
||||||
next: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function followed_tags(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case FOLLOWED_HASHTAGS_FETCH_REQUEST:
|
|
||||||
return state.set('isLoading', true);
|
|
||||||
case FOLLOWED_HASHTAGS_FETCH_SUCCESS:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
map.set('items', fromJS(action.followed_tags));
|
|
||||||
map.set('isLoading', false);
|
|
||||||
map.set('next', action.next);
|
|
||||||
});
|
|
||||||
case FOLLOWED_HASHTAGS_FETCH_FAIL:
|
|
||||||
return state.set('isLoading', false);
|
|
||||||
case FOLLOWED_HASHTAGS_EXPAND_REQUEST:
|
|
||||||
return state.set('isLoading', true);
|
|
||||||
case FOLLOWED_HASHTAGS_EXPAND_SUCCESS:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
map.update('items', set => set.concat(fromJS(action.followed_tags)));
|
|
||||||
map.set('isLoading', false);
|
|
||||||
map.set('next', action.next);
|
|
||||||
});
|
|
||||||
case FOLLOWED_HASHTAGS_EXPAND_FAIL:
|
|
||||||
return state.set('isLoading', false);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,7 +13,6 @@ import conversations from './conversations';
|
||||||
import custom_emojis from './custom_emojis';
|
import custom_emojis from './custom_emojis';
|
||||||
import { dropdownMenuReducer } from './dropdown_menu';
|
import { dropdownMenuReducer } from './dropdown_menu';
|
||||||
import filters from './filters';
|
import filters from './filters';
|
||||||
import followed_tags from './followed_tags';
|
|
||||||
import height_cache from './height_cache';
|
import height_cache from './height_cache';
|
||||||
import history from './history';
|
import history from './history';
|
||||||
import { listsReducer } from './lists';
|
import { listsReducer } from './lists';
|
||||||
|
@ -73,7 +72,6 @@ const reducers = {
|
||||||
markers: markersReducer,
|
markers: markersReducer,
|
||||||
picture_in_picture: pictureInPictureReducer,
|
picture_in_picture: pictureInPictureReducer,
|
||||||
history,
|
history,
|
||||||
followed_tags,
|
|
||||||
notificationPolicy: notificationPolicyReducer,
|
notificationPolicy: notificationPolicyReducer,
|
||||||
notificationRequests: notificationRequestsReducer,
|
notificationRequests: notificationRequestsReducer,
|
||||||
};
|
};
|
||||||
|
|
|
@ -8110,13 +8110,9 @@ noscript {
|
||||||
&__item {
|
&__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 15px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
gap: 15px;
|
gap: 8px;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -8223,7 +8219,7 @@ noscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
&--compact &__item {
|
&--compact &__item {
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user