mirror of
https://github.com/wassname/talk.git
synced 2026-07-15 11:26:58 +08:00
Merge branch 'master' into toxicity-detail
Conflicts: plugin-api/beta/client/components/index.js
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import {Button, Checkbox} from 'coral-ui';
|
||||
import QuestionBoxBuilder from './QuestionBoxBuilder';
|
||||
import cn from 'classnames';
|
||||
|
||||
import styles from './ConfigureCommentStream.css';
|
||||
|
||||
@@ -13,7 +14,7 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
<h3>{t('configure.title')}</h3>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.apply}
|
||||
className={cn(styles.apply, 'talk-embed-stream-configuration-submit-button')}
|
||||
onChange={handleChange}
|
||||
cStyle={changed ? 'green' : 'darkGrey'} >
|
||||
{t('configure.apply')}
|
||||
|
||||
@@ -102,7 +102,7 @@ class ConfigureStreamContainer extends Component {
|
||||
const closedTimeout = dirtySettings.closedTimeout;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='talk-embed-stream-configuration-container'>
|
||||
<ConfigureCommentStream
|
||||
handleChange={this.handleChange}
|
||||
handleApply={this.handleApply}
|
||||
|
||||
@@ -16,7 +16,7 @@ import mapValues from 'lodash/mapValues';
|
||||
import LoadMore from './LoadMore';
|
||||
import {getEditableUntilDate} from './util';
|
||||
import {findCommentWithId} from '../graphql/utils';
|
||||
import CommentContent from './CommentContent';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentTombstone from './CommentTombstone';
|
||||
import InactiveCommentLabel from './InactiveCommentLabel';
|
||||
@@ -529,7 +529,7 @@ export default class Comment extends React.Component {
|
||||
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
|
||||
{isActive &&
|
||||
<div className={'talk-stream-comment-actions-container'}>
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
{...slotProps}
|
||||
@@ -545,7 +545,7 @@ export default class Comment extends React.Component {
|
||||
/>
|
||||
</ActionButton>}
|
||||
</div>
|
||||
<div className="commentActionsRight comment__action-container">
|
||||
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
|
||||
<Slot
|
||||
fill="commentActions"
|
||||
wrapperComponent={ActionButton}
|
||||
|
||||
+3
-3
@@ -2,18 +2,18 @@ import React from 'react';
|
||||
|
||||
const CommentContent = ({comment}) => {
|
||||
const textbreaks = comment.body.split('\n');
|
||||
return <div className={`${name}-text`}>
|
||||
return <span className={`${name}-text`}>
|
||||
{
|
||||
textbreaks.map((line, i) => {
|
||||
return (
|
||||
<span key={i} className={`${name}-line`}>
|
||||
{line}
|
||||
<br className={`${name}-linebreak`}/>
|
||||
{i === textbreaks.length - 1 && <br className={`${name}-linebreak`}/>}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
</span>;
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
@@ -71,7 +71,7 @@ class ProfileContainer extends Component {
|
||||
const emailAddress = localProfile && localProfile.id;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='talk-embed-stream-profile-container'>
|
||||
<h2>{user.username}</h2>
|
||||
{emailAddress ? <p>{emailAddress}</p> : null}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Icon} from '../coral-ui';
|
||||
import styles from './Comment.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentTimestamp from 'coral-framework/components/CommentTimestamp';
|
||||
import CommentContent from '../coral-embed-stream/src/components/CommentContent';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import cn from 'classnames';
|
||||
import {getTotalReactionsCount} from 'coral-framework/utils';
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './styles.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {BASE_PATH} from 'coral-framework/constants/url';
|
||||
|
||||
const ModerationLink = (props) => props.isAdmin ? (
|
||||
<div className={styles.moderationLink}>
|
||||
<a href={`${BASE_PATH}admin/moderate/${props.assetId}`} target="_blank">
|
||||
<div className={cn(styles.moderationLink, 'talk-embed-stream-moderation-container')}>
|
||||
<a className='talk-embed-stream-moderation-link' href={`${BASE_PATH}admin/moderate/${props.assetId}`} target="_blank">
|
||||
{t('moderate_this_stream')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -94,14 +94,17 @@ const CONFIG = {
|
||||
MONGO_URL: process.env.TALK_MONGO_URL,
|
||||
REDIS_URL: process.env.TALK_REDIS_URL,
|
||||
|
||||
// REDIS_RECONNECTION_MAX_ATTEMPTS is the amount of attempts that a redis
|
||||
// connection will attempt to reconnect before aborting with an error.
|
||||
REDIS_RECONNECTION_MAX_ATTEMPTS: parseInt(process.env.TALK_REDIS_RECONNECTION_MAX_ATTEMPTS || '100'),
|
||||
// REDIS_CLIENT_CONFIG is the optional configuration that is merged with the
|
||||
// function config to provide deep control of the redis connection beheviour.
|
||||
REDIS_CLIENT_CONFIG: process.env.TALK_REDIS_CLIENT_CONFIGURATION || '{}',
|
||||
|
||||
// REDIS_RECONNECTION_MAX_RETRY_TIME is the time in string format for the
|
||||
// maximum amount of time that a client can be considered "connecting" before
|
||||
// attempts at reconnection are aborted with an error.
|
||||
REDIS_RECONNECTION_MAX_RETRY_TIME: ms(process.env.TALK_REDIS_RECONNECTION_MAX_RETRY_TIME || '1 min'),
|
||||
// REDIS_CLUSTER_MODE allows configuration on the type of cluster mode enabled
|
||||
// on the redis client. Can be either `NONE` or `CLUSTER`.
|
||||
REDIS_CLUSTER_MODE: process.env.TALK_REDIS_CLUSTER_MODE || 'NONE',
|
||||
|
||||
// REDIS_CLUSTER_CONFIGURATION contains the json string for the redis cluster
|
||||
// configuration.
|
||||
REDIS_CLUSTER_CONFIGURATION: process.env.TALK_REDIS_CLUSTER_CONFIGURATION || '[]',
|
||||
|
||||
// REDIS_RECONNECTION_BACKOFF_FACTOR is the factor that will be multiplied
|
||||
// against the current attempt count inbetween attempts to connect to redis.
|
||||
@@ -246,6 +249,26 @@ if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) {
|
||||
CONFIG.REDIS_URL = 'redis://localhost/1';
|
||||
}
|
||||
|
||||
// REDIS_CLUSTER_CONFIGURATION should be parsed when the cluster mode !== none.
|
||||
if (CONFIG.REDIS_CLUSTER_MODE === 'CLUSTER') {
|
||||
try {
|
||||
CONFIG.REDIS_CLUSTER_CONFIGURATION = JSON.parse(CONFIG.REDIS_CLUSTER_CONFIGURATION);
|
||||
} catch (err) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_CONFIGURATION is not valid JSON, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
|
||||
if (!Array.isArray(CONFIG.REDIS_CLUSTER_CONFIGURATION)) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_MODE is CLUSTER, but the TALK_REDIS_CLUSTER_CONFIGURATION is invalid, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
|
||||
if (CONFIG.REDIS_CLUSTER_CONFIGURATION.length === 0) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_CONFIGURATION must have at least one node specified in the cluster, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
}
|
||||
|
||||
// Client config is a JSON encoded string, defaulting to `{}`.
|
||||
CONFIG.REDIS_CLIENT_CONFIG = JSON.parse(CONFIG.REDIS_CLIENT_CONFIG);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Recaptcha configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -55,18 +55,21 @@ These are only used during the webpack build.
|
||||
#### Advanced
|
||||
{:.no_toc}
|
||||
|
||||
- `TALK_REDIS_RECONNECTION_MAX_ATTEMPTS` (_optional_) - the amount of attempts
|
||||
that a redis connection will attempt to reconnect before aborting with an
|
||||
error. (Default `100`)
|
||||
- `TALK_REDIS_RECONNECTION_MAX_RETRY_TIME` (_optional_) - the time in string
|
||||
format for the maximum amount of time that a client can be considered
|
||||
"connecting" before attempts at reconnection are aborted with an error.
|
||||
(Default `1 min`)
|
||||
- `TALK_REDIS_CLIENT_CONFIG` (_optional_) - configuration overrides for the
|
||||
redis client configuration in a JSON encoded string. Configuration is
|
||||
overridden as the second parameter to the redis client constructor, and is
|
||||
merged with default configuration. (Default `{}`)
|
||||
- `TALK_REDIS_RECONNECTION_BACKOFF_FACTOR` (_optional_) - the time factor that
|
||||
will be multiplied against the current attempt count inbetween attempts to
|
||||
connect to redis. (Default `500 ms`)
|
||||
- `TALK_REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME` (_optional_) - the minimum time
|
||||
used to delay before attempting to reconnect to redis. (Default `1 sec`)
|
||||
- `TALK_REDIS_CLUSTER_MODE` (_optional_) - the cluster mode of the redis client.
|
||||
Can be either `NONE` or `CLUSTER`. (Default `NONE`)
|
||||
- `TALK_REDIS_CLUSTER_CONFIGURATION` (_optional_) - the json serialized form of
|
||||
the cluster nodes. Only required when `TALK_REDIS_CLUSTER_MODE=CLUSTER`. See
|
||||
https://github.com/luin/ioredis#cluster for configuration details.
|
||||
(Default `[]`)
|
||||
|
||||
### Server
|
||||
|
||||
@@ -137,7 +140,7 @@ is not needed in most situations.
|
||||
use to set a cookie containing a JWT that was issued by Talk.
|
||||
(Default `process.env.TALK_JWT_COOKIE_NAME`)
|
||||
- `TALK_JWT_COOKIE_NAMES` (_optional_) - the different cookie names to check for
|
||||
a JWT token in, seperated by `,`. By default, we always use the
|
||||
a JWT token in, separated by `,`. By default, we always use the
|
||||
`process.env.TALK_JWT_COOKIE_NAME` and `process.env.TALK_JWT_SIGNING_COOKIE_NAME`
|
||||
for this value. Any additional cookie names specified here will be appended to
|
||||
the list of cookie names to inspect.
|
||||
|
||||
@@ -261,6 +261,7 @@ pt_BR:
|
||||
mod_faster: "Moderado mais rápido com atalhos de teclado"
|
||||
moderate: "Moderar →"
|
||||
more_detail: "Mais detalhes"
|
||||
new: "Novo"
|
||||
newest_first: "Mais novo primeiro"
|
||||
navigation: Navegação
|
||||
next_comment: "Vá para o próximo comentário"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "3.5.1",
|
||||
"version": "3.6.0",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
|
||||
@@ -5,3 +5,4 @@ export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNo
|
||||
export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName';
|
||||
export {default as CommentTimestamp} from 'coral-framework/components/CommentTimestamp';
|
||||
export {default as CommentDetail} from 'coral-framework/components/CommentDetail';
|
||||
export {default as CommentContent} from 'coral-framework/components/CommentContent';
|
||||
|
||||
@@ -5,20 +5,20 @@ const name = 'talk-plugin-comment-content';
|
||||
|
||||
const CommentContent = ({comment}) => {
|
||||
const textbreaks = comment.body.split('\n');
|
||||
return <div className={`${name}-text`}>
|
||||
return <span className={`${name}-text`}>
|
||||
{
|
||||
textbreaks.map((line, i) => {
|
||||
return (
|
||||
<span key={i} className={`${name}-line`}>
|
||||
<Linkify properties={{target: '_blank'}}>
|
||||
{line}
|
||||
{line.trim()}
|
||||
</Linkify>
|
||||
<br className={`${name}-linebreak`}/>
|
||||
{i !== textbreaks.length - 1 && <br className={`${name}-linebreak`}/>}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
</span>;
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './Comment.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Slot, CommentAuthorName, CommentTimestamp} from 'plugin-api/beta/client/components';
|
||||
import {Slot, CommentAuthorName, CommentTimestamp, CommentContent} from 'plugin-api/beta/client/components';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import {pluginName} from '../../package.json';
|
||||
import FeaturedButton from '../containers/FeaturedButton';
|
||||
@@ -19,9 +19,14 @@ class Comment extends React.Component {
|
||||
return (
|
||||
<div className={cn(styles.root, `${pluginName}-comment`)}>
|
||||
|
||||
<blockquote className={cn(styles.quote, `${pluginName}-comment-body`)}>
|
||||
{comment.body}
|
||||
</blockquote>
|
||||
<Slot
|
||||
component={'blockquote'}
|
||||
className={cn(styles.quote, `${pluginName}-comment-body`)}
|
||||
fill="commentContent"
|
||||
defaultComponent={CommentContent}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
|
||||
<div className={cn(`${pluginName}-comment-username-box`)}>
|
||||
|
||||
@@ -46,7 +51,7 @@ class Comment extends React.Component {
|
||||
</div>
|
||||
|
||||
<footer className={cn(styles.footer, `${pluginName}-comment-footer`)}>
|
||||
<div className={cn(styles.reactionsContainer, `${pluginName}-comment-reactions`)}>
|
||||
<div className={cn('talk-embed-stream-comment-actions-container-left', styles.reactionsContainer, `${pluginName}-comment-reactions`)}>
|
||||
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
@@ -64,7 +69,7 @@ class Comment extends React.Component {
|
||||
asset={asset}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(styles.actionsContainer, `${pluginName}-comment-actions`)}>
|
||||
<div className={cn('talk-embed-stream-comment-actions-container-right', styles.actionsContainer, `${pluginName}-comment-actions`)}>
|
||||
<button className={cn(styles.goTo, `${pluginName}-comment-go-to`)} onClick={this.viewComment}>
|
||||
<Icon name="forum" className={styles.repliesIcon} /> {comment.replyCount} | {t('talk-plugin-featured-comments.go_to_conversation')} <Icon name="keyboard_arrow_right" className={styles.goToIcon} />
|
||||
</button>
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class Tag extends React.Component {
|
||||
<span className={cn(styles.tagContainer, styles.noSelect)} onMouseEnter={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip} onTouchStart={this.showTooltip}
|
||||
onTouchEnd={this.hideTooltip} >
|
||||
<span className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip})}>
|
||||
<span className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip}, 'talk-stream-comment-featured-tag-label')}>
|
||||
{t('talk-plugin-featured-comments.featured')}
|
||||
</span>
|
||||
{tooltip && <Tooltip className={styles.tooltip} />}
|
||||
|
||||
@@ -35,7 +35,7 @@ class LikeButton extends React.Component {
|
||||
return (
|
||||
<div className={cn(styles.container, `${plugin}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.liked]: alreadyReacted}, `${plugin}-button`)}
|
||||
className={cn(styles.button, {[`${styles.liked} talk-plugin-like-liked`]: alreadyReacted}, `${plugin}-button`)}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span className={cn(`${plugin}-label`, styles.label)}>
|
||||
|
||||
@@ -2,12 +2,13 @@ import React from 'react';
|
||||
import styles from './OffTopicTag.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {isTagged} from 'plugin-api/beta/client/utils';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default (props) => (
|
||||
<span>
|
||||
{
|
||||
isTagged(props.comment.tags, 'OFF_TOPIC') && props.depth === 0 ? (
|
||||
<span className={styles.tag}>
|
||||
<span className={cn(styles.tag, 'talk-stream-comment-offtopic-tag-label')}>
|
||||
{t('off_topic')}
|
||||
</span>
|
||||
) : null
|
||||
|
||||
@@ -73,7 +73,9 @@ export default class PermalinkButton extends React.Component {
|
||||
ref={(ref) => this.linkButton = ref}
|
||||
onClick={this.toggle}
|
||||
className={cn(`${name}-button`, styles.button)}>
|
||||
{t('permalink')}
|
||||
<span className='talk-plugin-permalink-button-label'>
|
||||
{t('permalink')}
|
||||
</span>
|
||||
<Icon name="link" className={styles.icon}/>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class RespectButton extends React.Component {
|
||||
return (
|
||||
<div className={cn(styles.container, `${plugin}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.respected]: alreadyReacted}, `${plugin}-button`)}
|
||||
className={cn(styles.button, {[`${styles.respected} talk-plugin-respect-respected`]: alreadyReacted}, `${plugin}-button`)}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span className={cn(`${plugin}-label`, styles.label)}>
|
||||
|
||||
+23
-35
@@ -1,12 +1,14 @@
|
||||
const Redis = require('ioredis');
|
||||
const merge = require('lodash/merge');
|
||||
const debug = require('debug')('talk:services:redis');
|
||||
const enabled = require('debug').enabled('talk:services:redis');
|
||||
const {
|
||||
REDIS_URL,
|
||||
REDIS_RECONNECTION_MAX_ATTEMPTS,
|
||||
REDIS_RECONNECTION_MAX_RETRY_TIME,
|
||||
REDIS_RECONNECTION_BACKOFF_FACTOR,
|
||||
REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME,
|
||||
REDIS_CLIENT_CONFIG,
|
||||
REDIS_CLUSTER_MODE,
|
||||
REDIS_CLUSTER_CONFIGURATION,
|
||||
} = require('../config');
|
||||
|
||||
const attachMonitors = (client) => {
|
||||
@@ -16,8 +18,8 @@ const attachMonitors = (client) => {
|
||||
if (enabled) {
|
||||
client.on('connect', () => debug('client connected'));
|
||||
client.on('ready', () => debug('client ready'));
|
||||
client.on('reconnecting', () => debug('client connection lost, attempting to reconnect'));
|
||||
client.on('close', () => debug('client closed the connection'));
|
||||
client.on('reconnecting', () => debug('client connection lost, attempting to reconnect'));
|
||||
client.on('end', () => debug('client ended'));
|
||||
}
|
||||
|
||||
@@ -27,44 +29,30 @@ const attachMonitors = (client) => {
|
||||
console.error('Error connecting to redis:', err);
|
||||
}
|
||||
});
|
||||
client.on('node error', (err) => debug('node error', err));
|
||||
};
|
||||
|
||||
const connectionOptions = {
|
||||
retry_strategy: function(options) {
|
||||
if (options.error && options.error.code !== 'ECONNREFUSED') {
|
||||
function retryStrategy(times) {
|
||||
const delay = Math.max(times * REDIS_RECONNECTION_BACKOFF_FACTOR, REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME);
|
||||
|
||||
debug('retry strategy: none, an error occured');
|
||||
debug(`retry strategy: try to reconnect ${delay} ms from now`);
|
||||
|
||||
// End reconnecting on a specific error and flush all commands with a individual error
|
||||
return options.error;
|
||||
}
|
||||
if (options.total_retry_time > REDIS_RECONNECTION_MAX_RETRY_TIME) {
|
||||
|
||||
debug('retry strategy: none, exhausted retry time');
|
||||
|
||||
// End reconnecting after a specific timeout and flush all commands with a individual error
|
||||
return new Error('Retry time exhausted');
|
||||
}
|
||||
|
||||
if (options.attempt > REDIS_RECONNECTION_MAX_ATTEMPTS) {
|
||||
|
||||
debug('retry strategy: none, exhausted retry attempts');
|
||||
|
||||
// End reconnecting with built in error
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// reconnect after
|
||||
const delay = Math.max(options.attempt * REDIS_RECONNECTION_BACKOFF_FACTOR, REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME);
|
||||
|
||||
debug(`retry strategy: try to reconnect ${delay} ms from now`);
|
||||
|
||||
return delay;
|
||||
}
|
||||
};
|
||||
return delay;
|
||||
}
|
||||
|
||||
const createClient = () => {
|
||||
let client = new Redis(REDIS_URL, connectionOptions);
|
||||
let client;
|
||||
if (REDIS_CLUSTER_MODE === 'NONE') {
|
||||
client = new Redis(REDIS_URL, merge({}, REDIS_CLIENT_CONFIG, {
|
||||
retryStrategy,
|
||||
}));
|
||||
} else if (REDIS_CLUSTER_MODE === 'CLUSTER') {
|
||||
client = new Redis.Cluster(REDIS_CLUSTER_CONFIGURATION, merge({
|
||||
scaleReads: 'slave',
|
||||
}, REDIS_CLIENT_CONFIG, {
|
||||
clusterRetryStrategy: retryStrategy,
|
||||
}));
|
||||
}
|
||||
|
||||
// Attach the monitors that will print debug messages to the console.
|
||||
attachMonitors(client);
|
||||
|
||||
Reference in New Issue
Block a user