mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 03:34:45 +08:00
Merge branch 'master' into comment-tombstone-slot
This commit is contained in:
+52
-23
@@ -23,23 +23,33 @@ util.onshutdown([() => mongoose.disconnect()]);
|
||||
/**
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
async function listAssets() {
|
||||
async function listAssets(opts) {
|
||||
try {
|
||||
let assets = await AssetModel.find({}).sort({ created_at: 1 });
|
||||
|
||||
let table = new Table({
|
||||
head: ['ID', 'Title', 'URL'],
|
||||
});
|
||||
switch (opts.format) {
|
||||
case 'json': {
|
||||
console.log(JSON.stringify(assets, null, 2));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let table = new Table({
|
||||
head: ['ID', 'Title', 'URL'],
|
||||
});
|
||||
|
||||
assets.forEach(asset => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : '',
|
||||
]);
|
||||
});
|
||||
assets.forEach(asset => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : '',
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -49,12 +59,13 @@ async function listAssets() {
|
||||
|
||||
async function refreshAssets(ageString) {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
const query = AssetModel.find({}, { id: 1 });
|
||||
if (ageString) {
|
||||
// An age was specified, so filter only those assets.
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(Date.now() - ageMs);
|
||||
|
||||
let assets = await AssetModel.find(
|
||||
{
|
||||
query.merge({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
@@ -65,16 +76,28 @@ async function refreshAssets(ageString) {
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 1 }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Create a graph context.
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
// Load the assets.
|
||||
const cursor = query.cursor();
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
await Promise.all(assets.map(({ id }) => scraper.create(ctx, id)));
|
||||
console.log('Assets were queued to be scraped');
|
||||
const promises = [];
|
||||
|
||||
let asset = await cursor.next();
|
||||
while (asset) {
|
||||
promises.push(scraper.create(ctx, asset.id));
|
||||
asset = await cursor.next();
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
console.log(`${promises.length} Assets were queued to be scraped.`);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -202,11 +225,17 @@ async function rewrite(search, replace, options) {
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.option(
|
||||
'--format <type>',
|
||||
'Specify the output format [table]',
|
||||
/^(table|json)$/i,
|
||||
'table'
|
||||
)
|
||||
.description('list all the assets in the database')
|
||||
.action(listAssets);
|
||||
|
||||
program
|
||||
.command('refresh <age>')
|
||||
.command('refresh [age]')
|
||||
.description('queues the assets that exceed the age requested')
|
||||
.action(refreshAssets);
|
||||
|
||||
|
||||
+1
-1
@@ -328,7 +328,7 @@ program
|
||||
.action(searchUsers);
|
||||
|
||||
program
|
||||
.command('set-role <userID> <role>')
|
||||
.command('set-role <userID>')
|
||||
.description('sets the role on a user')
|
||||
.action(setUserRole);
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
:global {
|
||||
html, body, #root, #root > div {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #FAFAFA;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ToastContainer from './ToastContainer';
|
||||
import './App.css';
|
||||
import 'material-design-lite';
|
||||
|
||||
import AppRouter from '../AppRouter';
|
||||
|
||||
@@ -7,7 +7,6 @@ import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Logo } from './Logo';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
@@ -32,7 +31,6 @@ const CoralHeader = ({
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
<ModerationIndicator root={root} data={data} />
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
background-color: #E46D59;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: -4px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
@@ -2,21 +2,20 @@ import { gql } from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import Header from '../components/Header';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
// TODO: eventually we will readd modqueue counts
|
||||
// import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
export default withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Header($nullID: ID) {
|
||||
...${getDefinitionName(ModerationIndicator.fragments.root)}
|
||||
query TalkAdmin_Header {
|
||||
...${getDefinitionName(CommunityIndicator.fragments.root)}
|
||||
}
|
||||
${ModerationIndicator.fragments.root}
|
||||
${CommunityIndicator.fragments.root}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
variables: { nullID: null },
|
||||
// variables: { nullID: null },
|
||||
},
|
||||
}
|
||||
)(Header);
|
||||
|
||||
@@ -63,10 +63,6 @@ class Moderation extends Component {
|
||||
this.props.toggleStorySearch(true);
|
||||
};
|
||||
|
||||
getActiveTabCount = (props = this.props) => {
|
||||
return props.root[`${props.activeTab}Count`];
|
||||
};
|
||||
|
||||
moderate = accept => {
|
||||
const {
|
||||
acceptComment,
|
||||
@@ -139,12 +135,14 @@ class Moderation extends Component {
|
||||
|
||||
const comments = root[activeTab];
|
||||
|
||||
const activeTabCount = this.getActiveTabCount();
|
||||
const menuItems = Object.keys(queueConfig).map(queue => ({
|
||||
key: queue,
|
||||
name: queueConfig[queue].name,
|
||||
icon: queueConfig[queue].icon,
|
||||
count: root[`${queue}Count`],
|
||||
indicator:
|
||||
['premod', 'reported'].includes(queue) && root[queue].nodes.length > 0,
|
||||
// TODO: Eventually we'll reintroduce counting
|
||||
// count: root[`${props.queue}Count`]
|
||||
}));
|
||||
|
||||
const slotPassthrough = {
|
||||
@@ -189,7 +187,6 @@ class Moderation extends Component {
|
||||
loadMore={this.loadMore}
|
||||
commentBelongToQueue={this.props.commentBelongToQueue}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
selectCommentId={props.selectCommentId}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
import Indicator from '../../../components/Indicator';
|
||||
import styles from './ModerationMenu.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
import { Link } from 'react-router';
|
||||
@@ -32,7 +32,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => {
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name}{' '}
|
||||
<CountBadge count={queue.count} />
|
||||
{queue.indicator && <Indicator />}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -204,7 +204,7 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
const { commentCount, selectedCommentId } = this.props;
|
||||
const { selectedCommentId, hasNextPage } = this.props;
|
||||
|
||||
const switchedToMultiMode = prev.singleView && !this.props.singleView;
|
||||
const switchedMode = prev.singleView !== this.props.singleView;
|
||||
@@ -212,7 +212,6 @@ class ModerationQueue extends React.Component {
|
||||
prev.selectedCommentId !== selectedCommentId && selectedCommentId;
|
||||
const moderatedLastComment =
|
||||
prev.comments.length > 0 && this.getCommentCountWithoutDagling() === 0;
|
||||
const hasMoreComment = commentCount > 0;
|
||||
|
||||
if (switchedToMultiMode) {
|
||||
// Reflow virtual list.
|
||||
@@ -223,7 +222,7 @@ class ModerationQueue extends React.Component {
|
||||
this.scrollToSelectedComment();
|
||||
}
|
||||
|
||||
if (moderatedLastComment && hasMoreComment) {
|
||||
if (moderatedLastComment && hasNextPage) {
|
||||
this.props.loadMore();
|
||||
}
|
||||
}
|
||||
@@ -240,10 +239,7 @@ class ModerationQueue extends React.Component {
|
||||
const index = view.findIndex(
|
||||
({ id }) => id === this.props.selectedCommentId
|
||||
);
|
||||
if (
|
||||
index === view.length - 1 &&
|
||||
this.getCommentCountWithoutDagling() !== this.props.commentCount
|
||||
) {
|
||||
if (index === view.length - 1 && this.props.hasNextPage) {
|
||||
await this.props.loadMore();
|
||||
this.selectDown();
|
||||
return;
|
||||
@@ -467,7 +463,6 @@ ModerationQueue.propTypes = {
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
singleView: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
|
||||
@@ -314,11 +314,11 @@ class ModerationContainer extends Component {
|
||||
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
|
||||
if (premodEnabled && root.newCount === 0) {
|
||||
if (premodEnabled && root.new.nodes.length === 0) {
|
||||
delete currentQueueConfig.new;
|
||||
}
|
||||
|
||||
if (!premodEnabled && root.premodCount === 0) {
|
||||
if (!premodEnabled && root.premod.nodes.length === 0) {
|
||||
delete currentQueueConfig.premod;
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags, excludeDeleted: true}) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
@@ -456,7 +456,11 @@ const withModQueueQuery = withQuery(
|
||||
}
|
||||
`
|
||||
)}
|
||||
${Object.keys(queueConfig).map(
|
||||
${
|
||||
''
|
||||
/*
|
||||
TODO: eventually we'll reintroduce counting..
|
||||
Object.keys(queueConfig).map(
|
||||
queue => `
|
||||
${queue}Count: commentCount(query: {
|
||||
excludeDeleted: true,
|
||||
@@ -478,7 +482,8 @@ const withModQueueQuery = withQuery(
|
||||
asset_id: $asset_id,
|
||||
})
|
||||
`
|
||||
)}
|
||||
)*/
|
||||
}
|
||||
asset(id: $asset_id) @skip(if: $allAssets) {
|
||||
id
|
||||
title
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.commentSummary {
|
||||
|
||||
@@ -11,16 +11,6 @@ import { getTotalReactionsCount } from 'coral-framework/utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
goToStory = () => {
|
||||
this.props.navigate(this.props.comment.asset.url);
|
||||
};
|
||||
|
||||
goToConversation = () => {
|
||||
this.props.navigate(
|
||||
`${this.props.comment.asset.url}?commentId=${this.props.comment.id}`
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { comment, root } = this.props;
|
||||
const reactionCount = getTotalReactionsCount(comment.action_summaries);
|
||||
@@ -76,8 +66,8 @@ class Comment extends React.Component {
|
||||
<div className="my-comment-asset">
|
||||
<a
|
||||
className={cn(styles.assetURL, 'my-comment-anchor')}
|
||||
href="#"
|
||||
onClick={this.goToStory}
|
||||
href={this.props.comment.asset.url}
|
||||
target="_parent"
|
||||
>
|
||||
{t('common.story')}:{' '}
|
||||
{comment.asset.title ? comment.asset.title : comment.asset.url}
|
||||
@@ -87,7 +77,13 @@ class Comment extends React.Component {
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
<li>
|
||||
<a onClick={this.goToConversation} className={styles.viewLink}>
|
||||
<a
|
||||
className={styles.viewLink}
|
||||
href={`${this.props.comment.asset.url}?commentId=${
|
||||
this.props.comment.id
|
||||
}`}
|
||||
target="_parent"
|
||||
>
|
||||
<Icon name="open_in_new" className={styles.iconView} />
|
||||
{t('view_conversation')}
|
||||
</a>
|
||||
|
||||
@@ -37,7 +37,8 @@ export default class Popup extends Component {
|
||||
this.onBlur();
|
||||
};
|
||||
|
||||
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
|
||||
// Use `onunload` instead of `onbeforeunload` which is not supported in iOS
|
||||
// Safari.
|
||||
this.ref.onunload = () => {
|
||||
this.onUnload();
|
||||
|
||||
@@ -46,10 +47,15 @@ export default class Popup extends Component {
|
||||
}
|
||||
|
||||
this.resetCallbackInterval = setInterval(() => {
|
||||
if (this.ref && this.ref.onload === null) {
|
||||
clearInterval(this.resetCallbackInterval);
|
||||
this.resetCallbackInterval = null;
|
||||
this.setCallbacks();
|
||||
try {
|
||||
if (this.ref && this.ref.onload === null) {
|
||||
clearInterval(this.resetCallbackInterval);
|
||||
this.resetCallbackInterval = null;
|
||||
this.setCallbacks();
|
||||
}
|
||||
} catch (err) {
|
||||
// We could be getting a security exception here if the login page
|
||||
// gets redirected to another domain to authenticate.
|
||||
}
|
||||
}, 50);
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ const withSetUsername = hoistStatics(WrappedComponent => {
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
this.setState(changeSet);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,47 +3,53 @@ title: Trust
|
||||
permalink: /trust/
|
||||
---
|
||||
|
||||
Trust is a set of components within Talk that incorporate automated moderation
|
||||
features based on a user's previous behavior.
|
||||
Trust is a set of components within Talk that incorporate basic automated moderation features based on a user's previous behavior.
|
||||
|
||||
## User Karma Score
|
||||
|
||||
Using Trust’s calculations, Talk will automatically pre-moderate comments of
|
||||
users who have a negative karma score. All users start out with a `0` neutral
|
||||
karma score. If they have a comment approved by a moderator, their score
|
||||
increases by `1`; if they have a comment rejected by a moderator, it decreases
|
||||
by `1`. When a commenter is labeled as Unreliable, their comments must be
|
||||
moderated before they are posted.
|
||||
Using Trust’s calculations, Talk will automatically hold back, move to the Reported queue, and tag with a 'History' marker, any comments by users who have an Unreliable karma score. (This is for sites who practice post-moderation. If you set pre-moderation of all comments sitewide, this feature has limited use.)
|
||||
|
||||
When a commenter has one comment rejected, their next comment must be moderated
|
||||
once in order to post freely again. If they instead get rejected again, then
|
||||
they must have two of their comments approved in order to get added back to the
|
||||
queue.
|
||||
All users start out with a Neutral karma score (`0`). If they have a comment approved by a moderator, their score increases by `1`; if they have a comment rejected by a moderator, it decreases by `1`. When a commenter's score is labeled as Unreliable, their comments must be approved from the Reported queue before they are posted. Commenters are shown a message stating that a moderator will review their comment shortly.
|
||||
|
||||
Here are the default thresholds:
|
||||
|
||||
```text
|
||||
-2 and lower: Unreliable
|
||||
-1 to +2: Neutral
|
||||
+3 and higher: Reliable
|
||||
-1 and lower: Unreliable
|
||||
0 to +1: Neutral
|
||||
+2 and higher: Reliable (we don't do anything with this label right now)
|
||||
```
|
||||
|
||||
You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your
|
||||
configuration.
|
||||
So in this case, when a new commenter has their first comment rejected, their user karma score becomes `-1`, which triggers the Unreliable threshhold, and they must then have a comment approved by a moderator in order to post freely again. Until that occurs, all of their comments will be held back temporarily in the Reported queue, marked with a `History` tag.
|
||||
|
||||
If their next comment is also rejected, their user karma score is now `-2`, and they must have two comments approved in order to reach a Neutral score, and post without pre-approval.
|
||||
|
||||
We strongly recommend not telling your community how this system works, or where the threshholds lie. Firstly, they might try to game the system to meet approval, and secondly, it makes it harder for you to change the threshhold in the future. We suggest using language such as "We hold back comments for approval for a variety of reasons, including content, account history, and more."
|
||||
|
||||
If you see that a high proportion of first-time commenters on your site are abusive, you might want to change the threshhold to `0`, at least temporarily. You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your site configuration.
|
||||
|
||||
|
||||
## Reliable and Unreliable Flaggers
|
||||
|
||||
Trust also calculates how reliable users are in terms of the comments they
|
||||
report. This information is displayed to moderators in the User History drawer,
|
||||
which is accessed by clicking on a user’s name in the Admin.
|
||||
which is accessed by clicking on a user’s name in the Admin. Currently, no other action is taken based on this score.
|
||||
|
||||
If a user's reports mostly match what moderators reject, their Report status
|
||||
will display to moderators as Reliable in the user information drawer. If a
|
||||
user's reports mostly differ from what moderators reject, their Report status
|
||||
will show as Unreliable.
|
||||
|
||||
If we don't have enough reports to make a call, or the reports even out, their
|
||||
If Talk doesn't have enough reports to make a call, or the reports even out, their
|
||||
status is Neutral.
|
||||
|
||||
Here are the default thresholds:
|
||||
|
||||
```text
|
||||
-1 and lower: Unreliable
|
||||
0 to +1: Neutral
|
||||
+2 and higher: Reliable
|
||||
```
|
||||
You can configure your own Trust thresholds by using [TRUST_THRESHOLD](/talk/advanced-configuration/#trust-thresholds) in your
|
||||
configuration.
|
||||
|
||||
Note: Report Karma doesn't include reports of "I don't agree with this comment".
|
||||
|
||||
@@ -55,6 +55,18 @@ class SettingsLoader {
|
||||
// assembled Settings object.
|
||||
return zipObject(fields, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* get, like select, will retrieve the settings, but get will only return a
|
||||
* single setting.
|
||||
*
|
||||
* @param {String} field the field to get
|
||||
*/
|
||||
async get(field) {
|
||||
const value = await this._loader.load(field);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = () => ({ Settings: new SettingsLoader() });
|
||||
|
||||
@@ -57,8 +57,8 @@ const Comment = {
|
||||
asset({ asset_id }, _, { loaders: { Assets } }) {
|
||||
return Assets.getByID.load(asset_id);
|
||||
},
|
||||
async editing(comment, _, { loaders: { Settings } }) {
|
||||
const { editCommentWindowLength } = await Settings.select(
|
||||
editing: async (comment, _, { loaders: { Settings } }) => {
|
||||
const editCommentWindowLength = await Settings.get(
|
||||
'editCommentWindowLength'
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -162,7 +162,7 @@ en:
|
||||
suspect_word_title: "Suspect words list"
|
||||
suspect_word_text: "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list."
|
||||
tech_settings: "Tech Settings"
|
||||
organization_information: "Organization information"
|
||||
organization_information: "Organization Information"
|
||||
organization_info_copy: "We use this information in email notifications generated by Talk. This connects the messages to your organization, and provides a way for users to contact you if they have an issue with their account."
|
||||
organization_info_copy_2: "We recommend creating a generic email account (eg. community@yournewsroom.com) for this purpose. This means it can remain consistent over time, and doesn't expose a name that users could target if their account were blocked."
|
||||
organization_details: "Organization Details"
|
||||
@@ -274,7 +274,7 @@ en:
|
||||
comment: comment
|
||||
comment_is_ignored: "This comment is hidden because you ignored this user."
|
||||
comment_is_rejected: "You have rejected this comment."
|
||||
comment_is_deleted: "This comment was deleted."
|
||||
comment_is_deleted: "This commenter has deleted their account."
|
||||
comment_is_hidden: "This comment is not available."
|
||||
comments: comments
|
||||
configure_stream: "Configure"
|
||||
|
||||
@@ -94,9 +94,13 @@ const createResolveFactory = (() => {
|
||||
|
||||
module.exports = async (req, res, next) => {
|
||||
try {
|
||||
// Attach the custom css url.
|
||||
const { customCssUrl } = await SettingsService.select('customCssUrl');
|
||||
// Attach the custom css url and organization name.
|
||||
const { customCssUrl, organizationName } = await SettingsService.select(
|
||||
'customCssUrl',
|
||||
'organizationName'
|
||||
);
|
||||
res.locals.customCssUrl = customCssUrl;
|
||||
res.locals.organizationName = organizationName;
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
+6
-13
@@ -9,7 +9,8 @@ const Action = new Schema(
|
||||
id: {
|
||||
type: String,
|
||||
default: uuid.v4,
|
||||
unique: true,
|
||||
unique: 1,
|
||||
index: 1,
|
||||
},
|
||||
action_type: {
|
||||
type: String,
|
||||
@@ -19,7 +20,10 @@ const Action = new Schema(
|
||||
type: String,
|
||||
enum: ITEM_TYPES,
|
||||
},
|
||||
item_id: String,
|
||||
item_id: {
|
||||
type: String,
|
||||
index: 1,
|
||||
},
|
||||
user_id: String,
|
||||
|
||||
// The element that summaries will additionally group on in addtion to their action_type, item_type, and
|
||||
@@ -37,15 +41,4 @@ const Action = new Schema(
|
||||
}
|
||||
);
|
||||
|
||||
// Create an index on the `item_id` field so that queries looking for
|
||||
// actions based on the item id can resolve faster.
|
||||
Action.index(
|
||||
{
|
||||
item_id: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = Action;
|
||||
|
||||
+55
-104
@@ -55,12 +55,16 @@ const Comment = new Schema(
|
||||
type: String,
|
||||
default: uuid.v4,
|
||||
unique: true,
|
||||
index: true,
|
||||
},
|
||||
body: {
|
||||
type: String,
|
||||
},
|
||||
body_history: [BodyHistoryItemSchema],
|
||||
asset_id: String,
|
||||
asset_id: {
|
||||
type: String,
|
||||
index: true,
|
||||
},
|
||||
author_id: String,
|
||||
status_history: [Status],
|
||||
status: {
|
||||
@@ -90,7 +94,6 @@ const Comment = new Schema(
|
||||
// deleted_at stores the date that the given comment was deleted.
|
||||
deleted_at: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
|
||||
// Additional metadata stored on the field.
|
||||
@@ -110,95 +113,67 @@ const Comment = new Schema(
|
||||
}
|
||||
);
|
||||
|
||||
// Add the indexes for the id of the comment.
|
||||
Comment.index(
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
unique: true,
|
||||
background: false,
|
||||
}
|
||||
);
|
||||
|
||||
Comment.index(
|
||||
{
|
||||
status: 1,
|
||||
created_at: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
|
||||
Comment.index(
|
||||
{
|
||||
status: 1,
|
||||
created_at: 1,
|
||||
asset_id: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Create a sparse index to search across.
|
||||
Comment.index(
|
||||
{
|
||||
created_at: 1,
|
||||
'action_counts.flag': 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
sparse: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Create a sparse index to search across.
|
||||
Comment.index(
|
||||
{
|
||||
'action_counts.flag': 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
sparse: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Add an index that is optimized for finding flagged comments.
|
||||
Comment.index(
|
||||
{
|
||||
asset_id: 1,
|
||||
created_at: 1,
|
||||
'action_counts.flag': 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Add an index for the reply sort.
|
||||
Comment.index(
|
||||
{
|
||||
asset_id: 1,
|
||||
deleted_at: 1,
|
||||
created_at: -1,
|
||||
reply_count: -1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
{ partialFilterExpression: { deleted_at: null } }
|
||||
);
|
||||
|
||||
// Add an index that is optimized for finding a user's comments.
|
||||
Comment.index(
|
||||
{
|
||||
author_id: 1,
|
||||
deleted_at: 1,
|
||||
status: 1,
|
||||
created_at: -1,
|
||||
},
|
||||
{ partialFilterExpression: { deleted_at: null } }
|
||||
);
|
||||
|
||||
Comment.index({
|
||||
asset_id: 1,
|
||||
created_at: -1,
|
||||
});
|
||||
|
||||
Comment.index({
|
||||
asset_id: 1,
|
||||
created_at: 1,
|
||||
});
|
||||
|
||||
Comment.index({
|
||||
author_id: 1,
|
||||
created_at: -1,
|
||||
});
|
||||
|
||||
Comment.index({
|
||||
asset_id: 1,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
Comment.index({
|
||||
asset_id: 1,
|
||||
parent_id: 1,
|
||||
reply_count: -1,
|
||||
created_at: -1,
|
||||
});
|
||||
|
||||
Comment.index({
|
||||
asset_id: 1,
|
||||
reply_count: -1,
|
||||
created_at: -1,
|
||||
});
|
||||
|
||||
Comment.index(
|
||||
{
|
||||
'action_counts.flag': 1,
|
||||
status: 1,
|
||||
created_at: -1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
partialFilterExpression: {
|
||||
'action_counts.flag': { $exists: true, $gt: 0 },
|
||||
deleted_at: null,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -210,34 +185,10 @@ Comment.index(
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Optimize for tag searches/counts.
|
||||
Comment.index(
|
||||
{
|
||||
'tags.tag.name': 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
sparse: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Add an index that is optimized for sorting based on the created_at timestamp
|
||||
// but also good at locating comments that have a specific asset id.
|
||||
Comment.index(
|
||||
{
|
||||
asset_id: 1,
|
||||
created_at: 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
|
||||
Comment.virtual('edited').get(function() {
|
||||
return this.body_history.length > 1;
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ const Setting = new Schema(
|
||||
id: {
|
||||
type: String,
|
||||
default: '1',
|
||||
unique: 1,
|
||||
index: true,
|
||||
},
|
||||
moderation: {
|
||||
type: String,
|
||||
|
||||
+19
-28
@@ -58,6 +58,7 @@ const User = new Schema(
|
||||
default: uuid.v4,
|
||||
unique: true,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
|
||||
// This is sourced from the social provider or set manually during user setup
|
||||
@@ -107,6 +108,7 @@ const User = new Schema(
|
||||
status: {
|
||||
type: String,
|
||||
enum: USER_STATUS_USERNAME,
|
||||
index: true,
|
||||
},
|
||||
|
||||
// History stores the history of username status changes.
|
||||
@@ -135,6 +137,7 @@ const User = new Schema(
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
index: true,
|
||||
},
|
||||
history: [
|
||||
{
|
||||
@@ -226,41 +229,26 @@ User.index(
|
||||
}
|
||||
);
|
||||
|
||||
User.index(
|
||||
{
|
||||
lowercaseUsername: 1,
|
||||
'profiles.id': 1,
|
||||
created_at: -1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
User.index({
|
||||
lowercaseUsername: 1,
|
||||
'profiles.id': 1,
|
||||
created_at: -1,
|
||||
});
|
||||
|
||||
// This query is executed often, to count the number of flagged accounts with
|
||||
// usernames.
|
||||
User.index(
|
||||
{
|
||||
'action_counts.flag': 1,
|
||||
'status.username.status': 1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
User.index({
|
||||
'action_counts.flag': 1,
|
||||
'status.username.status': 1,
|
||||
});
|
||||
|
||||
// Sorting users by created at is the default people search.
|
||||
User.index(
|
||||
{
|
||||
created_at: -1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
User.index({
|
||||
created_at: -1,
|
||||
});
|
||||
|
||||
/**
|
||||
* returns true if a commenter is staff
|
||||
* returns true if a commenter is staff.
|
||||
*/
|
||||
User.method('isStaff', function() {
|
||||
return this.role !== 'COMMENTER';
|
||||
@@ -330,6 +318,9 @@ User.virtual('hasVerifiedEmail').get(function() {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* system returns true when the user is a system user.
|
||||
*/
|
||||
User.virtual('system')
|
||||
.get(function() {
|
||||
return this._system;
|
||||
|
||||
Generated
-27
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "4.3.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"exenv": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
|
||||
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
|
||||
},
|
||||
"react-side-effect": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.1.5.tgz",
|
||||
"integrity": "sha512-Z2ZJE4p/jIfvUpiUMRydEVpQRf2f8GMHczT6qLcARmX7QRb28JDBTpnM2g/i5y/p7ZDEXYGHWg0RbhikE+hJRw==",
|
||||
"requires": {
|
||||
"exenv": "1.2.2",
|
||||
"shallowequal": "1.0.2"
|
||||
}
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.2.tgz",
|
||||
"integrity": "sha512-zlVXeVUKvo+HEv1e2KQF/csyeMKx2oHvatQ9l6XjCUj3agvC8XGf6R9HvIPDSmp8FNPvx7b5kaEJTRi7CqxtEw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "4.4.0",
|
||||
"version": "4.4.1",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
@@ -219,6 +219,7 @@
|
||||
"babel-plugin-dynamic-import-node": "^1.1.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
||||
"browserstack-local": "^1.3.0",
|
||||
"casual": "^1.5.19",
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^6.0.0",
|
||||
"chai-datetime": "^1.5.0",
|
||||
|
||||
@@ -11,10 +11,22 @@ function getReactionConfig(reaction) {
|
||||
|
||||
if (CREATE_MONGO_INDEXES) {
|
||||
// Create the index on the comment model based on the reaction config.
|
||||
Comment.collection.createIndex(
|
||||
Comment.collection.ensureIndex(
|
||||
{
|
||||
created_at: 1,
|
||||
[`action_counts.${sc(reaction)}`]: 1,
|
||||
asset_id: 1,
|
||||
[`action_counts.${sc(reaction)}`]: -1,
|
||||
created_at: -1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
}
|
||||
);
|
||||
|
||||
Comment.collection.ensureIndex(
|
||||
{
|
||||
asset_id: 1,
|
||||
[`action_counts.${sc(reaction)}`]: -1,
|
||||
created_at: -1,
|
||||
},
|
||||
{
|
||||
background: true,
|
||||
|
||||
@@ -71,7 +71,7 @@ module.exports = {
|
||||
permalink: asset.url,
|
||||
comment_type: 'comment',
|
||||
comment_content: input.body,
|
||||
is_test: true,
|
||||
is_test: false,
|
||||
});
|
||||
|
||||
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as actions from './constants';
|
||||
|
||||
export const startAttach = () => ({
|
||||
type: actions.START_ATTACH,
|
||||
});
|
||||
|
||||
export const finishAttach = () => ({
|
||||
type: actions.FINISH_ATTACH,
|
||||
});
|
||||
@@ -45,6 +45,10 @@ class AddEmailAddressDialog extends React.Component {
|
||||
),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.startAttach();
|
||||
}
|
||||
|
||||
onChange = e => {
|
||||
const { name, value } = e.target;
|
||||
this.setState(
|
||||
@@ -99,7 +103,13 @@ class AddEmailAddressDialog extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
confirmChanges = async () => {
|
||||
finish = () => {
|
||||
this.props.finishAttach();
|
||||
};
|
||||
|
||||
confirmChanges = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.validate()) {
|
||||
this.showErrors();
|
||||
return;
|
||||
@@ -113,7 +123,11 @@ class AddEmailAddressDialog extends React.Component {
|
||||
email: emailAddress,
|
||||
password: confirmPassword,
|
||||
});
|
||||
this.props.notify('success', 'Email Added!');
|
||||
|
||||
this.props.notify(
|
||||
'success',
|
||||
t('talk-plugin-local-auth.add_email.added.alert')
|
||||
);
|
||||
this.goToNextStep();
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
@@ -143,13 +157,13 @@ class AddEmailAddressDialog extends React.Component {
|
||||
)}
|
||||
{step === 1 &&
|
||||
!settings.requireEmailConfirmation && (
|
||||
<EmailAddressAdded done={() => {}} />
|
||||
<EmailAddressAdded done={this.finish} />
|
||||
)}
|
||||
{step === 1 &&
|
||||
settings.requireEmailConfirmation && (
|
||||
<VerifyEmailAddress
|
||||
emailAddress={formData.emailAddress}
|
||||
done={() => {}}
|
||||
done={this.finish}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
@@ -161,6 +175,8 @@ AddEmailAddressDialog.propTypes = {
|
||||
attachLocalAuth: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
startAttach: PropTypes.func.isRequired,
|
||||
finishAttach: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddEmailAddressDialog;
|
||||
|
||||
@@ -41,7 +41,7 @@ const AddEmailContent = ({
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form autoComplete="off">
|
||||
<form autoComplete="off" onSubmit={confirmChanges}>
|
||||
<InputField
|
||||
id="emailAddress"
|
||||
label={t('talk-plugin-local-auth.add_email.enter_email_address')}
|
||||
@@ -82,12 +82,9 @@ const AddEmailContent = ({
|
||||
showSuccess={false}
|
||||
/>
|
||||
<div className={styles.actions}>
|
||||
<a
|
||||
className={cn(styles.button, styles.proceed)}
|
||||
onClick={confirmChanges}
|
||||
>
|
||||
<button className={cn(styles.button, styles.proceed)}>
|
||||
{t('talk-plugin-local-auth.add_email.add_email_address')}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,74 @@ import styles from './ChangeEmailContentDialog.css';
|
||||
import InputField from './InputField';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
|
||||
const initialState = {
|
||||
showError: false,
|
||||
formData: {
|
||||
confirmPassword: '',
|
||||
},
|
||||
errors: {},
|
||||
};
|
||||
|
||||
class ChangeEmailContentDialog extends React.Component {
|
||||
state = {
|
||||
showError: false,
|
||||
state = initialState;
|
||||
|
||||
clearForm = () => {
|
||||
this.setState(initialState);
|
||||
};
|
||||
|
||||
addError = err => {
|
||||
this.setState(({ errors }) => ({
|
||||
errors: { ...errors, ...err },
|
||||
}));
|
||||
};
|
||||
|
||||
removeError = errKey => {
|
||||
this.setState(state => {
|
||||
const { [errKey]: _, ...errors } = state.errors;
|
||||
return {
|
||||
errors,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
fieldValidation = (value, type, name) => {
|
||||
if (!value.length) {
|
||||
this.addError({
|
||||
[name]: t('talk-plugin-local-auth.change_password.required_field'),
|
||||
});
|
||||
} else if (!validate[type](value)) {
|
||||
this.addError({ [name]: errorMsj[type] });
|
||||
} else {
|
||||
this.removeError(name);
|
||||
}
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
const { name, value, type, dataset } = e.target;
|
||||
const validationType = dataset.validationType || type;
|
||||
|
||||
this.setState(
|
||||
state => ({
|
||||
formData: {
|
||||
...state.formData,
|
||||
[name]: value,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this.fieldValidation(value, validationType, name);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
hasError = err => {
|
||||
return Object.keys(this.state.errors).indexOf(err) !== -1;
|
||||
};
|
||||
|
||||
getError = errorKey => {
|
||||
return this.state.errors[errorKey];
|
||||
};
|
||||
|
||||
showError = () => {
|
||||
@@ -16,24 +80,31 @@ class ChangeEmailContentDialog extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.clearForm();
|
||||
this.props.closeDialog();
|
||||
};
|
||||
|
||||
confirmChanges = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const { confirmPassword = '' } = this.state.formData;
|
||||
|
||||
if (this.formHasError()) {
|
||||
this.showError();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.props.save();
|
||||
await this.props.save(confirmPassword);
|
||||
this.props.next();
|
||||
};
|
||||
|
||||
formHasError = () => this.props.hasError('confirmPassword');
|
||||
formHasError = () => this.hasError('confirmPassword');
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span className={styles.close} onClick={this.props.cancel}>
|
||||
<span className={styles.close} onClick={this.cancel}>
|
||||
×
|
||||
</span>
|
||||
<h1 className={styles.title}>
|
||||
@@ -59,17 +130,17 @@ class ChangeEmailContentDialog extends React.Component {
|
||||
label={t('talk-plugin-local-auth.change_email.enter_password')}
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
onChange={this.props.onChange}
|
||||
defaultValue=""
|
||||
hasError={this.props.hasError('confirmPassword')}
|
||||
errorMsg={this.props.getError('confirmPassword')}
|
||||
onChange={this.onChange}
|
||||
value={this.state.formData.confirmPassword}
|
||||
hasError={this.hasError('confirmPassword')}
|
||||
errorMsg={this.getError('confirmPassword')}
|
||||
showError={this.state.showError}
|
||||
columnDisplay
|
||||
/>
|
||||
<div className={styles.bottomActions}>
|
||||
<Button
|
||||
className={styles.cancel}
|
||||
onClick={this.props.cancel}
|
||||
onClick={this.cancel}
|
||||
type="button"
|
||||
>
|
||||
{t('talk-plugin-local-auth.change_email.cancel')}
|
||||
@@ -86,14 +157,11 @@ class ChangeEmailContentDialog extends React.Component {
|
||||
}
|
||||
|
||||
ChangeEmailContentDialog.propTypes = {
|
||||
save: PropTypes.func,
|
||||
next: PropTypes.func,
|
||||
cancel: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
save: PropTypes.func,
|
||||
formData: PropTypes.object,
|
||||
email: PropTypes.string,
|
||||
hasError: PropTypes.func,
|
||||
getError: PropTypes.func,
|
||||
closeDialog: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ChangeEmailContentDialog;
|
||||
|
||||
@@ -114,11 +114,11 @@ class Profile extends React.Component {
|
||||
|
||||
isSaveEnabled = () => {
|
||||
const { formData } = this.state;
|
||||
const { emailAddress, username } = this.props;
|
||||
const { root: { me: { username, email } } } = this.props;
|
||||
const formHasErrors = !!Object.keys(this.state.errors).length;
|
||||
const validUsername =
|
||||
formData.newUsername && formData.newUsername !== username;
|
||||
const validEmail = formData.newEmail && formData.newEmail !== emailAddress;
|
||||
const validEmail = formData.newEmail && formData.newEmail !== email;
|
||||
|
||||
return !formHasErrors && (validUsername || validEmail);
|
||||
};
|
||||
@@ -138,8 +138,8 @@ class Profile extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
saveEmail = async () => {
|
||||
const { newEmail, confirmPassword } = this.state.formData;
|
||||
saveEmail = async confirmPassword => {
|
||||
const { newEmail } = this.state.formData;
|
||||
|
||||
try {
|
||||
await this.props.updateEmailAddress({
|
||||
@@ -202,12 +202,10 @@ class Profile extends React.Component {
|
||||
)}
|
||||
<ChangeEmailContentDialog
|
||||
save={this.saveEmail}
|
||||
onChange={this.onChange}
|
||||
formData={this.state.formData}
|
||||
email={email}
|
||||
enable={formData.newEmail && email !== formData.newEmail}
|
||||
hasError={this.hasError}
|
||||
getError={this.getError}
|
||||
closeDialog={this.closeDialog}
|
||||
/>
|
||||
</ConfirmChangesDialog>
|
||||
|
||||
@@ -225,11 +223,21 @@ class Profile extends React.Component {
|
||||
disabled={!usernameCanBeUpdated}
|
||||
columnDisplay
|
||||
>
|
||||
<span className={styles.bottomText}>
|
||||
{t(
|
||||
'talk-plugin-local-auth.change_username.change_username_note'
|
||||
<div className={styles.bottomText}>
|
||||
<span>
|
||||
{t(
|
||||
'talk-plugin-local-auth.change_username.change_username_note'
|
||||
)}
|
||||
</span>
|
||||
{!usernameCanBeUpdated && (
|
||||
<b>
|
||||
{' '}
|
||||
{t(
|
||||
'talk-plugin-local-auth.change_username.is_not_eligible'
|
||||
)}
|
||||
</b>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</InputField>
|
||||
<InputField
|
||||
icon="email"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const prefix = 'TALK_LOCAL_AUTH';
|
||||
|
||||
export const START_ATTACH = `${prefix}_START_ATTACH`;
|
||||
export const FINISH_ATTACH = `${prefix}_FINISH_ATTACH`;
|
||||
@@ -3,10 +3,16 @@ import { bindActionCreators } from 'redux';
|
||||
import { connect, withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
|
||||
import AddEmailAddressDialog from '../components/AddEmailAddressDialog';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
import { withAttachLocalAuth } from '../hocs';
|
||||
import { startAttach, finishAttach } from '../actions';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
||||
const mapStateToProps = ({ talkPluginLocalAuth: state }) => ({
|
||||
inProgress: state.inProgress,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ notify, startAttach, finishAttach }, dispatch);
|
||||
|
||||
const withData = withFragments({
|
||||
root: gql`
|
||||
@@ -14,6 +20,13 @@ const withData = withFragments({
|
||||
me {
|
||||
id
|
||||
email
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
settings {
|
||||
requireEmailConfirmation
|
||||
@@ -23,8 +36,13 @@ const withData = withFragments({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withAttachLocalAuth,
|
||||
withData,
|
||||
excludeIf(({ root: { me } }) => !me || me.email)
|
||||
excludeIf(
|
||||
({ root: { me }, inProgress }) =>
|
||||
!me ||
|
||||
get(me, 'state.status.username.status') === 'UNSET' ||
|
||||
(me.email && !inProgress)
|
||||
)
|
||||
)(AddEmailAddressDialog);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import ChangePassword from './containers/ChangePassword';
|
||||
import AddEmailAddressDialog from './containers/AddEmailAddressDialog';
|
||||
import Profile from './containers/Profile';
|
||||
import translations from './translations.yml';
|
||||
import translations from '../translations.yml';
|
||||
import graphql from './graphql';
|
||||
import reducer from './reducer';
|
||||
|
||||
export default {
|
||||
reducer,
|
||||
translations,
|
||||
slots: {
|
||||
profileHeader: [Profile],
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as actions from './constants';
|
||||
|
||||
const initialState = {
|
||||
inProgress: false,
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.START_ATTACH:
|
||||
return {
|
||||
inProgress: true,
|
||||
};
|
||||
case actions.FINISH_ATTACH:
|
||||
return {
|
||||
inProgress: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const mutators = require('./server/mutators');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
translations: path.join(__dirname, 'server', 'translations.yml'),
|
||||
translations: path.join(__dirname, 'translations.yml'),
|
||||
typeDefs,
|
||||
mutators,
|
||||
resolvers,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
en:
|
||||
email:
|
||||
email_change_original:
|
||||
subject: Email change
|
||||
body: Your email address has been changed from {0} to {1}. If you did not initiate this change, please contact {2}. # TODO: update translation
|
||||
error:
|
||||
NO_LOCAL_PROFILE: No existing email address is associated with this account.
|
||||
LOCAL_PROFILE: An email address is already associated with this account.
|
||||
INCORRECT_PASSWORD: Provided password was incorrect.
|
||||
+14
-3
@@ -1,4 +1,12 @@
|
||||
en:
|
||||
email:
|
||||
email_change_original:
|
||||
subject: Email change
|
||||
body: Your email address has been changed from {0} to {1}. If you did not request this change, please contact {2}.
|
||||
error:
|
||||
NO_LOCAL_PROFILE: No existing email address is associated with this account.
|
||||
LOCAL_PROFILE: An email address is already associated with this account.
|
||||
INCORRECT_PASSWORD: Provided password was incorrect.
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
change_password: "Change Password"
|
||||
@@ -11,7 +19,8 @@ en:
|
||||
changed_password_msg: "Changed Password - Your password has been successfully changed"
|
||||
forgot_password_sent: "Forgot Password - We sent you an email to recover your password"
|
||||
change_username:
|
||||
change_username_note: "Usernames can only be changed once every 14 days. Your username is not currently eligible to be updated."
|
||||
change_username_note: "Usernames can only be changed once every 14 days."
|
||||
is_not_eligible: "You cannot currently change your username."
|
||||
save: "Save"
|
||||
edit_profile: "Edit Profile"
|
||||
cancel: "Cancel"
|
||||
@@ -43,7 +52,7 @@ en:
|
||||
email_does_not_match: "Email Address does not match"
|
||||
insert_password: "Insert Password:"
|
||||
required_field: "This field is required"
|
||||
done: "done"
|
||||
done: "Done"
|
||||
content:
|
||||
title: "Add an Email Address"
|
||||
description: "For your added security, we require users to add an email address to their accounts. Your email address will be used to:"
|
||||
@@ -59,6 +68,7 @@ en:
|
||||
subtitle: "Need to change your email address?"
|
||||
description_2: "You can change your account settings by visiting"
|
||||
path: "My Profile > Settings"
|
||||
alert: "Email Added!"
|
||||
es:
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
@@ -72,7 +82,8 @@ es:
|
||||
changed_password_msg: "Contraseña Actualizada - Tu contraseña ha sido exitosamente actualizada"
|
||||
forgot_password_sent: "Contraseña Olvidada - Te enviamos un email para recuperar tu contraseña"
|
||||
change_username:
|
||||
change_username_note: "El usuario puede ser cambiado cada 14 días"
|
||||
change_username_note: "El usuario puede ser cambiado cada 14 días."
|
||||
is_not_eligible: "Ahora mismo no se puede cambiar su nombre de usuario."
|
||||
save: "Guardar"
|
||||
edit_profile: "Editar Perfil"
|
||||
cancel: "Cancelar"
|
||||
@@ -1,12 +1,19 @@
|
||||
@custom-media --small-viewport (min-width: 425px);
|
||||
|
||||
.dialog {
|
||||
width: calc(100% - 50px);
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 380px;
|
||||
|
||||
top: 10px;
|
||||
font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
|
||||
@media (--small-viewport) {
|
||||
width: 380px;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
|
||||
@@ -7,7 +7,7 @@ en:
|
||||
date: "When you wrote the comment"
|
||||
url: "The permalink URL for the comment"
|
||||
body: "The comment text"
|
||||
asset_url: "The URL on the article or story where the comment appears"
|
||||
asset_url: "The URL of the article or story where the comment appears"
|
||||
confirm: "Download My Comment History"
|
||||
email:
|
||||
download:
|
||||
@@ -17,7 +17,7 @@ en:
|
||||
delete:
|
||||
subject: "Your account for {0} is scheduled to be deleted"
|
||||
body: |
|
||||
A request to delete your account was received. Your account is scheduled for deletion on {1}.
|
||||
A request to delete your account was received. Your account is scheduled for deletion on {1}.
|
||||
|
||||
After that time all of your comments will be removed from the site, all of your comments will be removed from our database, and your username and email address will be removed from our system.
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
@custom-media --narrow-viewport (max-width: 420px);
|
||||
|
||||
.commentContent {
|
||||
composes: content from "./CommentContent.css";
|
||||
|
||||
/* Prevent zoom on narrow viewports */
|
||||
@media (--narrow-viewport) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
|
||||
@@ -8,6 +8,7 @@ import RTE from './rte/RTE';
|
||||
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||
import { Bold, Italic, Blockquote } from './rte/features';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import bowser from 'bowser';
|
||||
|
||||
class Editor extends React.Component {
|
||||
ref = null;
|
||||
@@ -40,7 +41,9 @@ class Editor extends React.Component {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.props.isReply) {
|
||||
|
||||
// Skip IOS due to a bug, see https://www.pivotaltracker.com/story/show/157434928
|
||||
if (this.props.isReply && !bowser.ios) {
|
||||
this.ref.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
width: auto;
|
||||
max-width: 680px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.graphiql em {
|
||||
font-family: georgia;
|
||||
}
|
||||
+27
-18
@@ -1,49 +1,58 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const errors = require('../../errors');
|
||||
const Assets = require('../../services/assets');
|
||||
|
||||
const body =
|
||||
'Lorem ipsum dolor sponge amet, consectetur adipiscing clam. Ut lobortis sollicitudin pillar a ornare. Curabitur dignissim vestibulum cay non rhoncus. Cras laoreet ante vel nunc hendrerit, shelf imperdiet neque egestas. Suspendisse aliquet iaculis fermentum. Talk volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa, sed vehicula mauris velit non lectus. Integer non trust nec neque congue faucibus porttitor sit amet elkhorn.';
|
||||
const casual = require('casual');
|
||||
const { ErrNotFound } = require('../../errors');
|
||||
const Asset = require('../../models/asset');
|
||||
|
||||
router.get('/id/:asset_id', async (req, res, next) => {
|
||||
try {
|
||||
const asset = await Assets.findById(req.params.asset_id);
|
||||
const asset = await Asset.findOne({ id: req.params.asset_id });
|
||||
if (asset === null) {
|
||||
return next(errors.ErrNotFound);
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
res.render('dev/article', {
|
||||
title: asset.title,
|
||||
asset_id: asset.id,
|
||||
asset_url: asset.url,
|
||||
body: '',
|
||||
basePath: '/client/embed/stream',
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/random', (req, res) => {
|
||||
const title = casual.title;
|
||||
|
||||
res.redirect(`./title/${title.replace(/ /g, '-')}`);
|
||||
});
|
||||
|
||||
router.get('/title/:asset_title', (req, res) => {
|
||||
return res.render('dev/article', {
|
||||
res.render('dev/article', {
|
||||
title: req.params.asset_title.split('-').join(' '),
|
||||
asset_url: '',
|
||||
asset_id: null,
|
||||
body: body,
|
||||
basePath: '/client/embed/stream',
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
let skip = req.query.skip ? parseInt(req.query.skip) : 0;
|
||||
let limit = req.query.limit ? parseInt(req.query.limit) : 25;
|
||||
|
||||
try {
|
||||
const assets = await Assets.all(skip, limit);
|
||||
const skip = req.query.skip ? parseInt(req.query.skip) : 0;
|
||||
const limit = req.query.limit ? parseInt(req.query.limit) : 6;
|
||||
|
||||
const [assets, count] = await Promise.all([
|
||||
Asset.find({})
|
||||
.sort({ created_at: 1 })
|
||||
.limit(limit)
|
||||
.skip(skip),
|
||||
Asset.count(),
|
||||
]);
|
||||
|
||||
res.render('dev/articles', {
|
||||
assets: assets,
|
||||
skip,
|
||||
limit,
|
||||
count,
|
||||
assets,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -16,8 +16,6 @@ router.get('/', staticTemplate, async (req, res) => {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
asset_id: '',
|
||||
body: '',
|
||||
basePath: '/static/embed/stream',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -232,11 +232,5 @@ module.exports = class AssetsService {
|
||||
await AssetModel.remove({
|
||||
id: srcAssetID,
|
||||
});
|
||||
|
||||
// That's it!
|
||||
}
|
||||
|
||||
static all(limit = undefined) {
|
||||
return AssetModel.find({}).limit(limit);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
const Setting = require('../models/setting');
|
||||
const { ErrSettingsNotInit } = require('../errors');
|
||||
const { dotize } = require('./utils');
|
||||
const { isEmpty, zipObject, uniq } = require('lodash');
|
||||
const { isEmpty, zipObject } = require('lodash');
|
||||
const DataLoader = require('dataloader');
|
||||
|
||||
const selector = { id: '1' };
|
||||
|
||||
async function loadFn(fields = []) {
|
||||
const model = await Setting.findOne(selector).select(uniq(fields));
|
||||
async function loadFn(/* fields = [] */) {
|
||||
// Originally, we used the projection operation, turns out this isn't that
|
||||
// fast. We should utilize the redis cache instead here.
|
||||
// const model = await Setting.findOne(selector).select(uniq(fields));
|
||||
|
||||
const model = await Setting.findOne(selector);
|
||||
if (!model) {
|
||||
throw new ErrSettingsNotInit();
|
||||
}
|
||||
|
||||
+2
-7
@@ -3,16 +3,11 @@
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title>Talk - Coral Admin</title>
|
||||
<style media="screen">
|
||||
html, body, #root, #root > div { min-height: 100%; }
|
||||
body { margin: 0; background-color: #FAFAFA; font-family: 'Roboto', sans-serif; }
|
||||
</style>
|
||||
<%- include partials/head %>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.min.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<%- include partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-admin/bundle.css') %>">
|
||||
<%- include partials/custom-css %>
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -5,13 +5,17 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>GraphiQL</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<%- include ../partials/dev %>
|
||||
<style>
|
||||
html, body {
|
||||
html, body, #graphiql {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
</style>
|
||||
<link href="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.css" rel="stylesheet" />
|
||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
||||
@@ -20,6 +24,8 @@
|
||||
<script src="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<main id="graphiql"></main>
|
||||
<script>
|
||||
// Collect the URL parameters
|
||||
var parameters = {};
|
||||
@@ -112,8 +118,8 @@
|
||||
variables: null,
|
||||
operationName: null,
|
||||
}),
|
||||
document.body
|
||||
document.querySelector("#graphiql")
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+36
-44
@@ -8,53 +8,45 @@
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
main {
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
max-width:500px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<title><%= title %></title>
|
||||
<%- include ../partials/dev %>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1><%= title %></h1>
|
||||
<p><%= body %></p>
|
||||
<p><a href="<%= BASE_PATH %>admin">Admin</a> - <a href="<%= BASE_PATH %>dev/assets">All Assets</a></p>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script src="<%= resolve('embed.js') %>" async onload="
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '<%= BASE_URL %>',
|
||||
asset_url: '<%= asset_url ? asset_url : '' %>',
|
||||
asset_id: '<%= asset_id ? asset_id : '' %>',
|
||||
auth_token: '',
|
||||
/**
|
||||
* You can listen to events using the example below.
|
||||
* The argument passed is the event emitter from
|
||||
* https://github.com/asyncly/EventEmitter2
|
||||
*
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
*/
|
||||
plugins_config: {
|
||||
<%- include ../partials/dev-nav %>
|
||||
<div class="container">
|
||||
<h1 class="mt-3"><%= title %></h1>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script src="<%= resolve('embed.js') %>"></script>
|
||||
<script>
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '<%= BASE_URL %>',
|
||||
asset_url: '<%= asset_url ? asset_url : '' %>',
|
||||
asset_id: '<%= asset_id ? asset_id : '' %>',
|
||||
auth_token: '',
|
||||
/**
|
||||
* You can disable rendering slot components of a plugin by doing:
|
||||
*
|
||||
* 'talk-plugin-love': {
|
||||
* disable_components: true,
|
||||
* },
|
||||
*/
|
||||
test: 'data',
|
||||
debug: false
|
||||
}
|
||||
})
|
||||
"></script>
|
||||
</main>
|
||||
* You can listen to events using the example below.
|
||||
* The argument passed is the event emitter from
|
||||
* https://github.com/asyncly/EventEmitter2
|
||||
*
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
*/
|
||||
plugins_config: {
|
||||
/**
|
||||
* You can disable rendering slot components of a plugin by doing:
|
||||
*
|
||||
* 'talk-plugin-love': {
|
||||
* disable_components: true,
|
||||
* },
|
||||
*/
|
||||
test: 'data',
|
||||
debug: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+38
-12
@@ -1,14 +1,40 @@
|
||||
<html>
|
||||
<body>
|
||||
<h1>
|
||||
Asset list
|
||||
</h1>
|
||||
<% assets.forEach(function (asset) { %>
|
||||
<a href="<%= BASE_PATH %>dev/assets/id/<%= asset.id %>"><%= asset.url %></a><br />
|
||||
<% }) %>
|
||||
<p>
|
||||
(For dev use only. FYI, you can: ?skip=100&limit=25)
|
||||
</p>
|
||||
|
||||
</body>
|
||||
<head>
|
||||
<title>All Assets</title>
|
||||
<%- include ../partials/dev %>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<div class="container">
|
||||
<div class="d-flex w-100 justify-content-between mt-3 mb-2">
|
||||
<h1 class="mb-0">All Assets</h1>
|
||||
<span class="text-muted"><%= skip + 1 %> - <%= skip + assets.length %> of <%= count %> Assets</span>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<% if (skip === 0) { %><a href="<%= BASE_PATH %>dev/assets/random" class="list-group-item list-group-item-action list-group-item-primary"><i class="fa fa-plus" aria-hidden="true"></i> Create a random article</a><% } %>
|
||||
<% assets.forEach(function (asset) { %>
|
||||
<a href="<%= BASE_PATH %>dev/assets/id/<%= asset.id %>" class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1"><%= asset.title %></h5>
|
||||
<small>Created <%= asset.created_at.toLocaleString('en-US') %></small>
|
||||
</div>
|
||||
<small><%= asset.url %></small>
|
||||
</a>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% if (count !== assets.length) { %>
|
||||
<nav aria-label="Page navigation example" class="mt-2">
|
||||
<ul class="pagination justify-content-center">
|
||||
<% let page = 1; for (let i = 0; i < count; i += limit) { %>
|
||||
<% if (i === skip) { %>
|
||||
<li class="page-item disabled"><a class="page-link" href="#"><%= page %></a></li>
|
||||
<% } else { %>
|
||||
<li class="page-item"><a class="page-link" href="?skip=<%= i %>&limit=<%= limit %>"><%= page %></a></li>
|
||||
<% } %>
|
||||
<% page++; } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<%- include ../partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/default.css') %>">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/bundle.css') %>">
|
||||
<%- include ../partials/custom-css %>
|
||||
</head>
|
||||
<body class="embed-stream-page">
|
||||
<div id="talk-embed-stream-container"></div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<%- include partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-login/bundle.css') %>">
|
||||
<%- include partials/custom-css %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="talk-login-container"></div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<%- include ./head %>
|
||||
<%- include head %>
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
|
||||
<%- include custom-css %>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<% if (locals.customCssUrl) { %><link href="<%= customCssUrl %>" rel="stylesheet" type="text/css"><% } %>
|
||||
@@ -0,0 +1,8 @@
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="<%= BASE_PATH %>dev"><%= organizationName %> <span class="text-muted">Organization</span></a>
|
||||
<form class="form-inline mb-0">
|
||||
<a href="<%= BASE_PATH %>admin" class="btn btn-outline-primary mr-2"><i class="fa fa-lock" aria-hidden="true"></i> Admin</a>
|
||||
<a href="<%= BASE_PATH %>api/v1/graph/iql" class="btn btn-outline-secondary mr-2 graphiql"><i class="fa fa-terminal" aria-hidden="true"></i> Graph<em>i</em>QL</a>
|
||||
<a href="<%= BASE_PATH %>dev/assets" class="btn btn-outline-secondary"><i class="fa fa-list" aria-hidden="true"></i> All Assets</a>
|
||||
</form>
|
||||
</nav>
|
||||
@@ -0,0 +1,5 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous" rel="stylesheet">
|
||||
<link href="<%= BASE_PATH %>public/css/dev.css" rel="stylesheet" >
|
||||
@@ -18,9 +18,6 @@
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
|
||||
<%_ if (locals.customCssUrl) { _%>
|
||||
<link href="<%= customCssUrl %>" rel="stylesheet" type="text/css">
|
||||
<%_ } _%>
|
||||
<%- include data %>
|
||||
|
||||
<base href="<%= BASE_URL %>"/>
|
||||
|
||||
@@ -1828,6 +1828,13 @@ caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
|
||||
casual@^1.5.19:
|
||||
version "1.5.19"
|
||||
resolved "https://registry.yarnpkg.com/casual/-/casual-1.5.19.tgz#66fac46f7ae463f468f5913eb139f9c41c58bbf2"
|
||||
dependencies:
|
||||
mersenne-twister "^1.0.1"
|
||||
moment "^2.15.2"
|
||||
|
||||
center-align@^0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
|
||||
@@ -7154,6 +7161,10 @@ merge@^1.1.3:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
|
||||
|
||||
mersenne-twister@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a"
|
||||
|
||||
metascraper-author@^3.9.2:
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-3.9.2.tgz#ff2020ac428f59a875d655df3b0d4bea171fde19"
|
||||
@@ -7436,6 +7447,10 @@ moment@^2.10.3:
|
||||
version "2.19.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167"
|
||||
|
||||
moment@^2.15.2:
|
||||
version "2.22.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad"
|
||||
|
||||
mongodb-core@2.1.17:
|
||||
version "2.1.17"
|
||||
resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.17.tgz#a418b337a14a14990fb510b923dee6a813173df8"
|
||||
|
||||
Reference in New Issue
Block a user