mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4c4032b0d | ||
|
|
2ea0d51613 | ||
|
|
f2564f4eda | ||
|
|
73040b1314 | ||
|
|
c3c106c7b0 | ||
|
|
8faf69ce48 | ||
|
|
23f17db0c9 | ||
|
|
1385a0b961 | ||
|
|
7fd01e5845 | ||
|
|
340052cdf0 | ||
|
|
a085e4b6f9 | ||
|
|
1575b15e36 | ||
|
|
7da9126a76 | ||
|
|
9453d207f5 | ||
|
|
869c760a8b | ||
|
|
b283595c97 | ||
|
|
d672af63f9 | ||
|
|
b4ad78fd65 | ||
|
|
713de46c2a | ||
|
|
fc1e51ed62 | ||
|
|
4eef6f4218 | ||
|
|
c8cfd7c0f8 | ||
|
|
0e4cbf5f44 | ||
|
|
448b988281 | ||
|
|
7a688495f2 | ||
|
|
b9cf7084eb | ||
|
|
b99161f0cb | ||
|
|
6faa4862de | ||
|
|
eff3f1dfd8 | ||
|
|
73aa77d7da | ||
|
|
5580ab385f | ||
|
|
40eb0e97ec | ||
|
|
b7e1ce0205 | ||
|
|
bfde9bf8c7 | ||
|
|
54f9b28086 | ||
|
|
03a5eb7f9b | ||
|
|
43e60e6544 | ||
|
|
ebd16666e6 | ||
|
|
f2129082a9 | ||
|
|
1098c068f2 | ||
|
|
f0c95d0044 | ||
|
|
74226aa6fc | ||
|
|
b6ed5b792b | ||
|
|
d31969592c | ||
|
|
794487b1a9 | ||
|
|
39e45c30ba | ||
|
|
ed734ebb08 | ||
|
|
9f059e0c4a | ||
|
|
7d369f2d35 | ||
|
|
5fb5240f89 | ||
|
|
976931135c | ||
|
|
1e3d47eb87 | ||
|
|
2d804d783b | ||
|
|
3404578ecc | ||
|
|
48c5355a5b | ||
|
|
e3a300c643 | ||
|
|
a6653152e4 | ||
|
|
441422542c | ||
|
|
1d1822c086 | ||
|
|
1c2ff7d2ae | ||
|
|
9319bb4e1f | ||
|
|
6611f8b636 | ||
|
|
738542ab99 | ||
|
|
cd4ae47958 | ||
|
|
2ec66007d9 | ||
|
|
ea34c936d0 | ||
|
|
ef1fd9c159 | ||
|
|
c7ccf03132 | ||
|
|
8015098305 | ||
|
|
c85a2703f9 | ||
|
|
9aec6b026c | ||
|
|
b2ca9dba40 | ||
|
|
9186c73fff | ||
|
|
86d55fa2de | ||
|
|
12c4cfbbf6 | ||
|
|
c875a8d9ed | ||
|
|
4d082165aa | ||
|
|
08c01981ac |
+5
-4
@@ -1,6 +1,7 @@
|
||||
# Mozilla Community Participation Guidelines
|
||||
# Vox Media Code of Conduct
|
||||
|
||||
## Version 2.3 - Updated May 16, 2017
|
||||
## Introduction
|
||||
|
||||
The heart of Mozilla is people. We put people first and do our best to recognize, appreciate and respect the diversity of our global contributors. The Mozilla Project welcomes contributions from everyone who shares our goals and wants to contribute in a healthy and constructive manner within our community. As such, we have adopted this code of conduct and require all those who participate to agree and adhere to these Community Participation Guidelines in order to help us create a safe and positive community experience for all.
|
||||
https://www.mozilla.org/en-US/about/governance/policies/participation/
|
||||
This code of conduct governs the environment of the Vox Product team. We created it not because we anticipate bad behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team and because having a code provides us with clear avenues to correct our culture should it ever stray from that course. We make this code public in the hopes of contributing to the ongoing conversation about inclusion in the tech, design, and media communities and encourage other teams to fork it and make it their own. To our team, we commit to enforce and evolve this code as our team grows.
|
||||
|
||||
Read the rest here: http://code-of-conduct.voxmedia.com/
|
||||
|
||||
+3
-23
@@ -18,7 +18,7 @@ There are at least three ways to contribute to Talk:
|
||||
|
||||
Conversation surrounding contributions begins in [issues](https://github.com/coralproject/talk/issues).
|
||||
|
||||
### When should I Create an Issue?
|
||||
### When should I create an issue?
|
||||
|
||||
File an issue as soon as you have an idea of something you'd like to contribute. We would love to hear what you're thinking and help refine the idea to make it into the Talk ecosystem.
|
||||
|
||||
@@ -30,9 +30,9 @@ Please file issues if:
|
||||
|
||||
### What should I include?
|
||||
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a story or this form:
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a user story in this form:
|
||||
|
||||
> As a [type of person] I'd like to be able to [do something] so that I can [get some result].
|
||||
> As a [type of Coral user] I'd like to [do something] so that I can [get some result/value].
|
||||
|
||||
This exercise does two things:
|
||||
|
||||
@@ -71,23 +71,3 @@ If you'd like to discuss a contribution, please [file an issue](https://github.c
|
||||
Talk's translations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
|
||||
|
||||
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repository.
|
||||
|
||||
## I want to contribute but I'm not sure what to do!
|
||||
|
||||
If you want to contribute but don't have a clear idea of exactly what that may be, here are some resources that may help:
|
||||
|
||||
### Product Roadmap
|
||||
|
||||
Please visit our product roadmap here: https://www.pivotaltracker.com/n/projects/1863625. If you'd like to take on any of our scheduled tasks we'd be forever grateful!
|
||||
|
||||
### Discussion Forum
|
||||
|
||||
If you'd like to discuss what we're up to, please visit or [community](https://community.coralproject.net/) or [contact us](https://coralproject.net/contact).
|
||||
|
||||
### Integrations
|
||||
|
||||
Have a favorite analytics engine? Data science service? CMS? Auth platform? Deployment platform or pipeline? Pet project? Consider building a plugin to integrate them!
|
||||
|
||||
### Favorite Features?
|
||||
|
||||
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2018 Mozilla Foundation
|
||||
Copyright 2019 Vox Media, Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](CONTRIBUTING.md#pull-requests)
|
||||
|
||||
---
|
||||
|
||||
> :construction: **NOTE:** We're currently building v5 of this platform, check it out in our [next branch](https://github.com/coralproject/talk/tree/next) :construction_worker_man: :construction_worker_woman:
|
||||
|
||||
---
|
||||
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/talk).
|
||||
|
||||
Built with <3 by The Coral Project, a part of [Vox Media](https://www.voxmedia.com/a/go-deeper/about).
|
||||
Built with <3 by The Coral Project, a part of [Vox Media](https://www.voxmedia.com/).
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@ import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
FETCH_ASSETS_SUCCESS,
|
||||
FETCH_ASSETS_FAILURE,
|
||||
LOAD_MORE_ASSETS_REQUEST,
|
||||
LOAD_MORE_ASSETS_SUCCESS,
|
||||
LOAD_MORE_ASSETS_FAILURE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_CRITERIA,
|
||||
UPDATE_ASSET_STATE_REQUEST,
|
||||
UPDATE_ASSET_STATE_SUCCESS,
|
||||
UPDATE_ASSET_STATE_FAILURE,
|
||||
UPDATE_ASSETS,
|
||||
} from '../constants/stories';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -21,17 +23,14 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
|
||||
export const fetchAssets = (query = {}) => (dispatch, _, { rest2 }) => {
|
||||
dispatch({ type: FETCH_ASSETS_REQUEST });
|
||||
return rest(`/assets?${queryString.stringify(query)}`)
|
||||
.then(({ result, page, count, limit, totalPages }) =>
|
||||
return rest2(`/stories?${queryString.stringify(query)}`)
|
||||
.then(({ edges, pageInfo }) =>
|
||||
dispatch({
|
||||
type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages,
|
||||
edges,
|
||||
pageInfo,
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
@@ -43,12 +42,31 @@ export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const loadMoreAssets = (query = {}) => (dispatch, _l, { rest2 }) => {
|
||||
dispatch({ type: LOAD_MORE_ASSETS_REQUEST });
|
||||
return rest2(`/stories?${queryString.stringify(query)}`)
|
||||
.then(({ edges, pageInfo }) =>
|
||||
dispatch({
|
||||
type: LOAD_MORE_ASSETS_SUCCESS,
|
||||
edges,
|
||||
pageInfo,
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: LOAD_MORE_ASSETS_FAILURE, error: errorMessage });
|
||||
});
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
|
||||
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
|
||||
dispatch({ type: UPDATE_ASSET_STATE_REQUEST });
|
||||
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
|
||||
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
|
||||
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS, id, closedAt }))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
@@ -58,10 +76,6 @@ export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAssets = assets => dispatch => {
|
||||
dispatch({ type: UPDATE_ASSETS, assets });
|
||||
};
|
||||
|
||||
export const setPage = page => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
|
||||
@@ -73,7 +73,7 @@ class AdminLogin extends React.Component {
|
||||
this.setState({ requestPassword: true });
|
||||
}}
|
||||
>
|
||||
{t('login.request_passowrd')}
|
||||
{t('login.request_password')}
|
||||
</a>
|
||||
</p>
|
||||
{loginMaxExceeded && (
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
background-color: #696969;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.header > div {
|
||||
@@ -41,7 +42,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 170px;
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@
|
||||
background-color: transparent;
|
||||
transition: background-color 200ms;
|
||||
font-weight: 100;
|
||||
letter-spacing: .8;
|
||||
letter-spacing: 0.8;
|
||||
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
|
||||
@@ -79,7 +79,7 @@ class SignIn extends React.Component {
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
{t('login.request_passowrd')}
|
||||
{t('login.request_password')}
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
color: #063b9a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
@@ -109,7 +109,7 @@
|
||||
}
|
||||
|
||||
.external {
|
||||
font-size: .7em;
|
||||
font-size: 0.7em;
|
||||
text-decoration: none;
|
||||
color: #063b9a;
|
||||
cursor: pointer;
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
> i {
|
||||
@@ -139,3 +139,25 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.bodyHistoryToggle {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.editedCommentBody {
|
||||
padding: 0 5px 5px 5px;
|
||||
}
|
||||
|
||||
.editedComment {
|
||||
margin-top: 6px;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import TimeAgo from 'coral-framework/components/TimeAgo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetailComment extends React.Component {
|
||||
state = { showingEditHistory: false };
|
||||
|
||||
approve = () =>
|
||||
this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
@@ -29,6 +31,29 @@ class UserDetailComment extends React.Component {
|
||||
? null
|
||||
: this.props.rejectComment({ commentId: this.props.comment.id });
|
||||
|
||||
getBodyHistory = () => {
|
||||
const bodyHistory = [];
|
||||
const comment = this.props.comment;
|
||||
for (let i = 0; i < comment.body_history.length - 1; i++) {
|
||||
bodyHistory.push(
|
||||
<div key={i} className={styles.editedComment}>
|
||||
<div>
|
||||
<TimeAgo className={styles.created} datetime={comment.created_at} />
|
||||
</div>
|
||||
<div className={styles.editedCommentBody}>
|
||||
{comment.body_history[i].body}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return bodyHistory;
|
||||
};
|
||||
|
||||
toggleEditHistory = () => {
|
||||
this.setState({ showingEditHistory: !this.state.showingEditHistory });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
comment,
|
||||
@@ -81,6 +106,14 @@ class UserDetailComment extends React.Component {
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
<span
|
||||
className={styles.bodyHistoryToggle}
|
||||
onClick={this.toggleEditHistory}
|
||||
>
|
||||
{this.state.showingEditHistory
|
||||
? t('comment.hide_edit_history')
|
||||
: t('comment.show_edit_history')}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -142,6 +175,14 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
|
||||
{this.state.showingEditHistory ? (
|
||||
<div className={styles.container}>
|
||||
{t('comment.edit_history')}
|
||||
{this.getBodyHistory()}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CommentDetails root={root} comment={comment} />
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -4,12 +4,14 @@ export const FETCH_ASSETS_REQUEST = `${prefix}_FETCH_ASSETS_REQUEST`;
|
||||
export const FETCH_ASSETS_SUCCESS = `${prefix}_FETCH_ASSETS_SUCCESS`;
|
||||
export const FETCH_ASSETS_FAILURE = `${prefix}_FETCH_ASSETS_FAILURE`;
|
||||
|
||||
export const LOAD_MORE_ASSETS_REQUEST = `${prefix}_LOAD_MORE_ASSETS_REQUEST`;
|
||||
export const LOAD_MORE_ASSETS_SUCCESS = `${prefix}_LOAD_MORE_ASSETS_SUCCESS`;
|
||||
export const LOAD_MORE_ASSETS_FAILURE = `${prefix}_LOAD_MORE_ASSETS_FAILURE`;
|
||||
|
||||
export const UPDATE_ASSET_STATE_REQUEST = `${prefix}_UPDATE_ASSET_STATE_REQUEST`;
|
||||
export const UPDATE_ASSET_STATE_SUCCESS = `${prefix}_UPDATE_ASSET_STATE_SUCCESS`;
|
||||
export const UPDATE_ASSET_STATE_FAILURE = `${prefix}_UPDATE_ASSET_STATE_FAILURE`;
|
||||
|
||||
export const UPDATE_ASSETS = `${prefix}_UPDATE_ASSETS`;
|
||||
|
||||
export const SET_PAGE = `${prefix}_SET_PAGE`;
|
||||
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
|
||||
export const SET_CRITERIA = `${prefix}_SET_CRITERIA`;
|
||||
|
||||
@@ -42,6 +42,10 @@ export default withFragments({
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
body_history {
|
||||
body
|
||||
created_at
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
...${getDefinitionName(CommentDetails.fragments.comment)}
|
||||
|
||||
@@ -3,57 +3,79 @@ import update from 'immutability-helper';
|
||||
|
||||
const initialState = {
|
||||
assets: {
|
||||
byId: {},
|
||||
ids: [],
|
||||
assets: [],
|
||||
edges: [],
|
||||
pageInfo: {},
|
||||
},
|
||||
searchValue: '',
|
||||
criteria: {
|
||||
asc: 'false',
|
||||
filter: 'all',
|
||||
limit: 20,
|
||||
},
|
||||
loading: true,
|
||||
loadingMore: false,
|
||||
};
|
||||
|
||||
export default function assets(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FETCH_ASSETS_SUCCESS: {
|
||||
const assets = action.assets.reduce((prev, curr) => {
|
||||
prev[curr.id] = curr;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
case actions.FETCH_ASSETS_REQUEST: {
|
||||
return update(state, {
|
||||
loading: { $set: true },
|
||||
});
|
||||
}
|
||||
case actions.FETCH_ASSETS_FAILURE: {
|
||||
return update(state, {
|
||||
loading: { $set: false },
|
||||
});
|
||||
}
|
||||
case actions.FETCH_ASSETS_SUCCESS: {
|
||||
return update(state, {
|
||||
loading: { $set: false },
|
||||
assets: {
|
||||
totalPages: { $set: action.totalPages },
|
||||
page: { $set: action.page },
|
||||
byId: { $set: assets },
|
||||
count: { $set: action.count },
|
||||
ids: { $set: Object.keys(assets) },
|
||||
edges: { $set: action.edges },
|
||||
pageInfo: { $set: action.pageInfo },
|
||||
},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_REQUEST:
|
||||
case actions.LOAD_MORE_ASSETS_REQUEST: {
|
||||
return update(state, {
|
||||
loadingMore: { $set: true },
|
||||
});
|
||||
}
|
||||
case actions.LOAD_MORE_ASSETS_FAILURE: {
|
||||
return update(state, {
|
||||
loadingMore: { $set: false },
|
||||
});
|
||||
}
|
||||
case actions.LOAD_MORE_ASSETS_SUCCESS: {
|
||||
return update(state, {
|
||||
loadingMore: { $set: false },
|
||||
assets: {
|
||||
edges: { $push: action.edges },
|
||||
pageInfo: {
|
||||
endCursor: { $set: action.pageInfo.endCursor },
|
||||
hasNextPage: { $set: action.pageInfo.hasNextPage },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_SUCCESS:
|
||||
const index = state.assets.edges.findIndex(
|
||||
({ node: { id } }) => id === action.id
|
||||
);
|
||||
if (index < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return update(state, {
|
||||
assets: {
|
||||
byId: {
|
||||
[action.id]: {
|
||||
closedAt: { $set: action.closedAt },
|
||||
edges: {
|
||||
[index]: {
|
||||
node: {
|
||||
closedAt: { $set: action.closedAt },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.UPDATE_ASSETS:
|
||||
return update(state, {
|
||||
assets: {
|
||||
assets: { $set: action.assets },
|
||||
},
|
||||
});
|
||||
case actions.SET_PAGE:
|
||||
return {
|
||||
...state,
|
||||
page: action.page,
|
||||
};
|
||||
case actions.SET_SEARCH_VALUE:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -140,8 +140,11 @@ const LOAD_MORE_QUERY = gql`
|
||||
$limit: Int
|
||||
$cursor: Cursor
|
||||
$value: String
|
||||
$state: UserStateInput
|
||||
) {
|
||||
users(query: { value: $value, limit: $limit, cursor: $cursor }) {
|
||||
users(
|
||||
query: { value: $value, limit: $limit, cursor: $cursor, state: $state }
|
||||
) {
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
|
||||
@@ -14,6 +14,11 @@ class TechSettings extends React.Component {
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateCustomAdminCssUrl = event => {
|
||||
const updater = { customAdminCssUrl: { $set: event.target.value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateDomainlist = (listName, list) => {
|
||||
this.props.updatePending({
|
||||
updater: {
|
||||
@@ -50,6 +55,14 @@ class TechSettings extends React.Component {
|
||||
onChange={this.updateCustomCssUrl}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard title={t('configure.custom_admin_css_url')}>
|
||||
<p>{t('configure.custom_admin_css_url_desc')}</p>
|
||||
<input
|
||||
className={styles.customCSSInput}
|
||||
value={settings.customAdminCssUrl}
|
||||
onChange={this.updateCustomAdminCssUrl}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<Slot fill="adminTechSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ export default compose(
|
||||
settings: gql`
|
||||
fragment TalkAdmin_TechSettings_settings on Settings {
|
||||
customCssUrl
|
||||
customAdminCssUrl
|
||||
domains {
|
||||
whitelist
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
font-weight: 300;
|
||||
margin-bottom: 8px;
|
||||
overflow-wrap: break-word;
|
||||
word-break:break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.body {
|
||||
@@ -103,7 +103,7 @@
|
||||
color: #063b9a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
@@ -111,14 +111,14 @@
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
&:hover {
|
||||
opacity: .9;
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #393B44;
|
||||
color: #393b44;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
@@ -127,12 +127,12 @@
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.external {
|
||||
font-size: .7em;
|
||||
font-size: 0.7em;
|
||||
text-decoration: none;
|
||||
color: #063b9a;
|
||||
cursor: pointer;
|
||||
@@ -140,7 +140,7 @@
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
opacity: 0.9;
|
||||
}
|
||||
i {
|
||||
font-size: 12px;
|
||||
@@ -196,3 +196,25 @@
|
||||
.commentContentFooter {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bodyHistoryToggle {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.editedCommentBody {
|
||||
padding: 0 5px 5px 5px;
|
||||
}
|
||||
|
||||
.editedComment {
|
||||
margin-top: 6px;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import t from 'coral-framework/services/i18n';
|
||||
class Comment extends React.Component {
|
||||
ref = null;
|
||||
|
||||
state = { showingEditHistory: false };
|
||||
|
||||
handleRef = ref => (this.ref = ref);
|
||||
|
||||
handleFocusOrClick = () => {
|
||||
@@ -45,6 +47,30 @@ class Comment extends React.Component {
|
||||
? null
|
||||
: this.props.rejectComment({ commentId: this.props.comment.id });
|
||||
|
||||
getBodyHistory = () => {
|
||||
const bodyHistory = [];
|
||||
const comment = this.props.comment;
|
||||
for (let i = 0; i < comment.body_history.length - 1; i++) {
|
||||
bodyHistory.push(
|
||||
<div key={i} className={styles.editedComment}>
|
||||
<div>
|
||||
<TimeAgo className={styles.created} datetime={comment.created_at} />
|
||||
</div>
|
||||
<div className={styles.editedCommentBody}>
|
||||
{comment.body_history[i].body}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return bodyHistory;
|
||||
};
|
||||
|
||||
toggleEditHistory = () => {
|
||||
this.setState({ showingEditHistory: !this.state.showingEditHistory });
|
||||
this.props.clearHeightCache && this.props.clearHeightCache();
|
||||
};
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
if (!prev.selected && this.props.selected) {
|
||||
this.ref.focus();
|
||||
@@ -137,6 +163,14 @@ class Comment extends React.Component {
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
<span
|
||||
className={styles.bodyHistoryToggle}
|
||||
onClick={this.toggleEditHistory}
|
||||
>
|
||||
{this.state.showingEditHistory
|
||||
? t('comment.hide_edit_history')
|
||||
: t('comment.show_edit_history')}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
@@ -201,6 +235,14 @@ class Comment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
|
||||
{this.state.showingEditHistory ? (
|
||||
<div className={styles.container}>
|
||||
{t('comment.edit_history')}
|
||||
{this.getBodyHistory()}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CommentDetails
|
||||
root={root}
|
||||
comment={comment}
|
||||
|
||||
@@ -52,6 +52,10 @@ export default withFragments({
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
body_history {
|
||||
body
|
||||
created_at
|
||||
}
|
||||
hasParent
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -95,3 +94,11 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.loadMore {
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadMoreSpinner {
|
||||
margin: 20px auto 50px;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ import React, { Component } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Link } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, Option, Paginate, Icon } from 'coral-ui';
|
||||
import { DataTable, TableHeader, RadioGroup, Radio } from 'react-mdl';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './Stories.css';
|
||||
import { Dropdown, Option, Icon, Spinner } from 'coral-ui';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
import LoadMore from 'coral-admin/src/components/LoadMore';
|
||||
|
||||
import styles from './Stories.css';
|
||||
|
||||
class Stories extends Component {
|
||||
renderDate = date => {
|
||||
@@ -39,10 +42,11 @@ class Stories extends Component {
|
||||
filter,
|
||||
onSearchChange,
|
||||
onSettingChange,
|
||||
onPageChange,
|
||||
asc,
|
||||
onLoadMore,
|
||||
loading,
|
||||
loadingMore,
|
||||
} = this.props;
|
||||
const rows = assets.ids.map(id => assets.byId[id]);
|
||||
const rows = assets.edges.map(({ node }) => node);
|
||||
|
||||
return (
|
||||
<div className={cn('talk-admin-stories', styles.container)}>
|
||||
@@ -74,60 +78,58 @@ class Stories extends Component {
|
||||
<Radio value="open">{t('streams.open')}</Radio>
|
||||
<Radio value="closed">{t('streams.closed')}</Radio>
|
||||
</RadioGroup>
|
||||
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
|
||||
<RadioGroup
|
||||
name="sortBy"
|
||||
value={asc}
|
||||
childContainer="div"
|
||||
onChange={onSettingChange('asc')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value="false">{t('streams.newest')}</Radio>
|
||||
<Radio value="true">{t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{rows.length ? (
|
||||
<div className={styles.mainContent}>
|
||||
<DataTable className={styles.streamsTable} rows={rows}>
|
||||
<TableHeader name="title" cellFormatter={this.renderTitle}>
|
||||
{t('streams.article')}
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="publication_date"
|
||||
cellFormatter={this.renderDate}
|
||||
>
|
||||
{t('streams.pubdate')}
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="closedAt"
|
||||
cellFormatter={this.renderStatus}
|
||||
className={styles.status}
|
||||
>
|
||||
{t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
<Paginate
|
||||
pageCount={assets.totalPages}
|
||||
page={assets.page - 1}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyCard>{t('streams.empty_result')}</EmptyCard>
|
||||
)}
|
||||
<div className={styles.mainContent}>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : rows.length ? (
|
||||
<div>
|
||||
<DataTable className={styles.streamsTable} rows={rows}>
|
||||
<TableHeader name="title" cellFormatter={this.renderTitle}>
|
||||
{t('streams.article')}
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="publication_date"
|
||||
cellFormatter={this.renderDate}
|
||||
>
|
||||
{t('streams.pubdate')}
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="closedAt"
|
||||
cellFormatter={this.renderStatus}
|
||||
className={styles.status}
|
||||
>
|
||||
{t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
{loadingMore ? (
|
||||
<Spinner className={styles.loadMoreSpinner} />
|
||||
) : (
|
||||
<LoadMore
|
||||
showLoadMore={assets.pageInfo.hasNextPage}
|
||||
loadMore={onLoadMore}
|
||||
className={styles.loadMore}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyCard>{t('streams.empty_result')}</EmptyCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Stories.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
loadingMore: PropTypes.bool,
|
||||
assets: PropTypes.object,
|
||||
searchValue: PropTypes.string,
|
||||
asc: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
onLoadMore: PropTypes.func.isRequired,
|
||||
onStatusChange: PropTypes.func.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
onSettingChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import { bindActionCreators } from 'redux';
|
||||
import {
|
||||
fetchAssets,
|
||||
updateAssetState,
|
||||
setPage,
|
||||
setSearchValue,
|
||||
setCriteria,
|
||||
loadMoreAssets,
|
||||
} from 'coral-admin/src/actions/stories';
|
||||
import Stories from '../components/Stories';
|
||||
|
||||
@@ -35,13 +35,11 @@ class StoriesContainer extends Component {
|
||||
};
|
||||
|
||||
fetchAssets = query => {
|
||||
const { searchValue, asc, filter, limit } = this.props;
|
||||
const { searchValue: value, filter } = this.props;
|
||||
|
||||
this.props.fetchAssets({
|
||||
value: searchValue,
|
||||
asc,
|
||||
value,
|
||||
filter,
|
||||
limit,
|
||||
...query,
|
||||
});
|
||||
};
|
||||
@@ -51,30 +49,39 @@ class StoriesContainer extends Component {
|
||||
|
||||
try {
|
||||
await updateAssetState(id, closeStream ? Date.now() : null);
|
||||
this.fetchAssets();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
onPageChange = ({ selected }) => {
|
||||
const page = selected + 1;
|
||||
this.props.setPage(page);
|
||||
this.fetchAssets({ page });
|
||||
onLoadMore = async () => {
|
||||
const {
|
||||
searchValue: value,
|
||||
filter,
|
||||
assets: {
|
||||
pageInfo: { endCursor: cursor },
|
||||
},
|
||||
loadMoreAssets,
|
||||
} = this.props;
|
||||
try {
|
||||
await loadMoreAssets({ cursor, value, filter });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Stories
|
||||
loading={this.props.loading}
|
||||
loadingMore={this.props.loadingMore}
|
||||
assets={this.props.assets}
|
||||
searchValue={this.props.searchValue}
|
||||
asc={this.props.asc}
|
||||
filter={this.props.filter}
|
||||
limit={this.props.limit}
|
||||
onPageChange={this.onPageChange}
|
||||
onStatusChange={this.onStatusChange}
|
||||
onSettingChange={this.onSettingChange}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onLoadMore={this.onLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -82,34 +89,34 @@ class StoriesContainer extends Component {
|
||||
|
||||
const mapStateToProps = ({ stories }) => ({
|
||||
assets: stories.assets,
|
||||
loading: stories.loading,
|
||||
loadingMore: stories.loadingMore,
|
||||
searchValue: stories.searchValue,
|
||||
asc: stories.criteria.asc,
|
||||
filter: stories.criteria.filter,
|
||||
limit: stories.criteria.limit,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
setPage,
|
||||
setCriteria,
|
||||
setSearchValue,
|
||||
fetchAssets,
|
||||
updateAssetState,
|
||||
loadMoreAssets,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
StoriesContainer.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
loadingMore: PropTypes.bool,
|
||||
assets: PropTypes.object,
|
||||
searchValue: PropTypes.string,
|
||||
asc: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
limit: PropTypes.number,
|
||||
setPage: PropTypes.func.isRequired,
|
||||
setCriteria: PropTypes.func.isRequired,
|
||||
setSearchValue: PropTypes.func.isRequired,
|
||||
fetchAssets: PropTypes.func.isRequired,
|
||||
loadMoreAssets: PropTypes.func.isRequired,
|
||||
updateAssetState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class EmbedContainer extends React.Component {
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_username_has_been_rejected'));
|
||||
notify('info', t('your_username_has_been_rejected_not_in_line'));
|
||||
props.updateStatus(state.status);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ class ChangeUsername extends Component {
|
||||
return (
|
||||
<RestrictedMessageBox>
|
||||
<div className="talk-change-username">
|
||||
<span>{t('framework.edit_name.msg')}</span>
|
||||
<span>{t('framework.edit_name.rejected')}</span>
|
||||
<div className={styles.alert}>{alert}</div>
|
||||
<label htmlFor="username" className="screen-reader-text">
|
||||
{t('framework.edit_name.label')}
|
||||
|
||||
@@ -3,6 +3,7 @@ import URLSearchParams from '@ungap/url-search-params';
|
||||
import pym from 'pym.js';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import { buildUrl } from 'coral-framework/utils/url';
|
||||
|
||||
import SnackBar from './SnackBar';
|
||||
import onIntersect from './onIntersect';
|
||||
import {
|
||||
@@ -92,6 +93,21 @@ function viewportDimensions() {
|
||||
};
|
||||
}
|
||||
|
||||
function parseAMPHash(opts) {
|
||||
const result = { ...opts };
|
||||
const query = window.location.hash.length && window.location.hash.substr(1);
|
||||
if (query) {
|
||||
const parsed = queryString.parse(query);
|
||||
if (parsed.asset_url && !result.asset_url) {
|
||||
result.asset_url = parsed.asset_url;
|
||||
}
|
||||
if (parsed.asset_id && !result.asset_id) {
|
||||
result.asset_id = parsed.asset_id;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default class Bridge {
|
||||
constructor(
|
||||
element,
|
||||
@@ -110,19 +126,23 @@ export default class Bridge {
|
||||
lazy = process.env.TALK_DEFAULT_LAZY_RENDER === 'TRUE',
|
||||
// Any additional options are extracted to be sent to the embed via the
|
||||
// pym bridge.
|
||||
amp,
|
||||
...opts
|
||||
}
|
||||
) {
|
||||
this.pym = null;
|
||||
this.element = element;
|
||||
this.opts = opts;
|
||||
this.amp = amp;
|
||||
this.lazy = !amp && lazy;
|
||||
|
||||
// Parse amp hash.
|
||||
this.opts = amp ? parseAMPHash(opts) : opts;
|
||||
this.query = buildQuery(this.opts);
|
||||
this.emitter = new EventEmitter({ wildcard: true });
|
||||
this.snackBar = new SnackBar(snackBarStyles || {});
|
||||
this.snackBar = amp ? null : new SnackBar(snackBarStyles || {});
|
||||
this.onAuthChanged = onAuthChanged;
|
||||
this.talkBaseUrl = ensureEndSlash(talkBaseUrl);
|
||||
this.talkStaticUrl = ensureEndSlash(talkStaticUrl);
|
||||
this.lazy = lazy;
|
||||
|
||||
// Store queued operations in a queue that can be processed once the stream
|
||||
// is rendered.
|
||||
@@ -179,6 +199,16 @@ export default class Bridge {
|
||||
if (height !== cachedHeight) {
|
||||
this.pym.el.firstChild.style.height = `${height}px`;
|
||||
cachedHeight = height;
|
||||
if (this.amp) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
sentinel: 'amp',
|
||||
type: 'embed-size',
|
||||
height,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -267,8 +297,10 @@ export default class Bridge {
|
||||
// Setup Pym.
|
||||
this.setupPym();
|
||||
|
||||
// Attach the snackBar to the pym parent and to the body of the page.
|
||||
this.snackBar.attach(window.document.body, this.pym);
|
||||
if (this.snackBar) {
|
||||
// Attach the snackBar to the pym parent and to the body of the page.
|
||||
this.snackBar.attach(window.document.body, this.pym);
|
||||
}
|
||||
|
||||
// If the user clicks outside the embed, then tell the embed.
|
||||
document.addEventListener('click', this.handleClick.bind(this), true);
|
||||
@@ -315,7 +347,10 @@ export default class Bridge {
|
||||
this.emitter.removeAllListeners();
|
||||
|
||||
// Remove the snackbar.
|
||||
this.snackBar.remove();
|
||||
if (this.snackBar) {
|
||||
this.snackBar.remove();
|
||||
this.snackBar = null;
|
||||
}
|
||||
|
||||
// Remove the pym parent.
|
||||
this.pym.remove();
|
||||
|
||||
@@ -56,6 +56,7 @@ export const Talk = {
|
||||
* @param {String} [config.auth_token] - (optional) A jwt representing the session
|
||||
* @param {String} [config.lazy] - (optional) If set the stream will only render lazily
|
||||
* @param {String} [config.talkStaticUrl] - (optional) Static URL used to serve Talk
|
||||
* @param {Boolean} [config.amp] - (optional) Run Talk in AMP mode
|
||||
* @return {Object}
|
||||
*/
|
||||
render: (element, config) => {
|
||||
|
||||
@@ -159,6 +159,11 @@ export async function createContext({
|
||||
token,
|
||||
});
|
||||
|
||||
const rest2 = createRestClient({
|
||||
uri: `${BASE_PATH}api/v2`,
|
||||
token,
|
||||
});
|
||||
|
||||
const staticConfig = getStaticConfiguration();
|
||||
let { LIVE_URI: liveUri, BASE_ORIGIN: origin } = staticConfig;
|
||||
if (liveUri == null) {
|
||||
@@ -193,6 +198,7 @@ export async function createContext({
|
||||
plugins,
|
||||
eventEmitter,
|
||||
rest,
|
||||
rest2,
|
||||
graphql,
|
||||
notification,
|
||||
localStorage,
|
||||
|
||||
@@ -43,6 +43,32 @@ const CONFIG = {
|
||||
process.env.TALK_WHITELISTED_LANGUAGES &&
|
||||
process.env.TALK_WHITELISTED_LANGUAGES.split(',').map(l => l.trim()),
|
||||
|
||||
// USERNAME_CAST_REGEXP defiles the regex expression that will be used to
|
||||
// strip characters from a username during a username cast operation.
|
||||
USERNAME_CAST_REGEXP: new RegExp(
|
||||
process.env.USERNAME_CAST_REGEXP || '[^a-zA-Z_]',
|
||||
'g'
|
||||
),
|
||||
|
||||
// USERNAME_REPLACEMENT_CAST_REGEXP defiles the regex expression that will be
|
||||
// used to replace characters with the replacement character during a username
|
||||
// cast operation. First duplicates will be replaced, then
|
||||
USERNAME_REPLACEMENT_CAST_REGEXP: new RegExp(
|
||||
process.env.USERNAME_REPLACEMENT_CAST_REGEXP || ' +',
|
||||
'g'
|
||||
),
|
||||
|
||||
// USERNAME_REPLACEMENT_CHARACTER is the character used to replace other
|
||||
// characters matching the USERNAME_REPLACEMENT_CAST_REGEXP.
|
||||
USERNAME_REPLACEMENT_CHARACTER:
|
||||
process.env.USERNAME_REPLACEMENT_CHARACTER || '_',
|
||||
|
||||
// USERNAME_VALIDATION_REGEX defines the allowed characters for a username in
|
||||
// Talk.
|
||||
USERNAME_VALIDATION_REGEX: new RegExp(
|
||||
process.env.USERNAME_VALIDATION_REGEX || '^[A-Za-z0-9_]+$'
|
||||
),
|
||||
|
||||
// When TRUE, it ensures that database indexes created in core will not add
|
||||
// indexes.
|
||||
CREATE_MONGO_INDEXES: process.env.DISABLE_CREATE_MONGO_INDEXES !== 'TRUE',
|
||||
|
||||
+3
-3
@@ -76,9 +76,7 @@ sidebar:
|
||||
- title: GitHub
|
||||
url: https://github.com/coralproject/
|
||||
- title: Docker
|
||||
url: https://hub.docker.com/r/coralproject/
|
||||
- title: Roadmap
|
||||
url: https://www.pivotaltracker.com/n/projects/1863625
|
||||
url: https://hub.docker.com/r/coralproject/talk/
|
||||
side:
|
||||
- title: Installation
|
||||
children:
|
||||
@@ -122,6 +120,8 @@ sidebar:
|
||||
url: /integrating/translations-i18n/
|
||||
- title: GDPR Compliance
|
||||
url: /integrating/gdpr/
|
||||
- title: Accelerated Mobile Page
|
||||
url: /integrating/amp/
|
||||
- title: Product Guide
|
||||
children:
|
||||
- title: How Talk Works
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"hexo": {
|
||||
"version": "3.7.1"
|
||||
"version": "3.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "hexo serve",
|
||||
|
||||
@@ -5,9 +5,18 @@ permalink: /
|
||||
|
||||
Online comments are broken. Our open-source Talk tool rethinks how moderation,
|
||||
comment display, and conversation function, creating the opportunity for safer,
|
||||
smarter discussions around your work. Read more about our product features and
|
||||
goals [here](https://coralproject.net/talk). The
|
||||
documentation available here is pertaining to the technical details for
|
||||
smarter discussions around your work.
|
||||
|
||||
More than 60 newsrooms use Talk to run their on-site communities, including the Washington Post, the Wall Street Journal, and IGN.
|
||||
|
||||
[Read more about our product features and
|
||||
goals here](https://coralproject.net/talk).
|
||||
|
||||
<div class="callout">
|
||||
We offer hosting and support packages for Talk. [Contact us for more information.](https://coralproject.net/pricing/)
|
||||
</div>
|
||||
|
||||
The documentation available here is pertaining to the technical details for
|
||||
installing, configuring, and deploying Talk.
|
||||
|
||||
Talk is a [Node](https://nodejs.org/) application with
|
||||
@@ -46,7 +55,7 @@ Start by making a new directory and create a file called `docker-compose.yml` an
|
||||
version: '2'
|
||||
services:
|
||||
talk:
|
||||
image: coralproject/talk:4.5
|
||||
image: coralproject/talk:4
|
||||
restart: always
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
@@ -43,7 +43,7 @@ be used to setup Talk:
|
||||
version: '2'
|
||||
services:
|
||||
talk:
|
||||
image: coralproject/talk:4.5
|
||||
image: coralproject/talk:4
|
||||
restart: always
|
||||
ports:
|
||||
- "3000:3000"
|
||||
@@ -121,7 +121,7 @@ base installation with additional custom plugins. Images can be created with the
|
||||
most basic of `Dockerfile`'s:
|
||||
|
||||
```docker
|
||||
FROM coralproject/talk:4.5-onbuild
|
||||
FROM coralproject/talk:4-onbuild
|
||||
```
|
||||
|
||||
And running the following to build the docker image:
|
||||
@@ -153,7 +153,7 @@ your containerized infrastructure. The versioning of our Docker tags as well
|
||||
lets you do something like:
|
||||
|
||||
```docker
|
||||
FROM coralproject/talk:4.5-onbuild
|
||||
FROM coralproject/talk:4-onbuild
|
||||
```
|
||||
|
||||
Which would pin your image to `4.5.x release's.
|
||||
Which would pin your image to `4.x.x release's.
|
||||
|
||||
@@ -13,6 +13,9 @@ permalink: /pre-launch-checklist/
|
||||
- [ ] Do you have a domain name for Talk?
|
||||
- **Recommended**: You should host Talk on a subdomain on your main site (e.g. if your site is mysitefornews.org, you should serve Talk from a subdomain like talk.mysitefornews.org) to avoid issues with third-party cookie sharing.
|
||||
|
||||
|
||||
- [ ] Do you have a Content Delivery Network (CDN) to improve performance and reduce load on your Talk servers?
|
||||
|
||||
|
||||
- [ ] Do you have a MongoDB instance?
|
||||
- A MongoDB [Docker instance](/talk/installation-from-docker/) hosted alongside Talk?
|
||||
@@ -28,9 +31,11 @@ permalink: /pre-launch-checklist/
|
||||
- [ ] Do you want to provide single sign-on (SSO) by integrating with an external auth system?
|
||||
- See [Authenticating with Talk](/talk/integrating/authentication/)
|
||||
|
||||
|
||||
- [ ] Do you want to integrate Talk with your CMS to automate embedding Talk Comment Stream into your site?
|
||||
- See [CMS Integration](/talk/integrating/cms-integration/)
|
||||
|
||||
|
||||
- [ ] Do you want to use Social sign-on?
|
||||
- Facebook
|
||||
- Install [talk-plugin-facebook-auth](/talk/plugin/talk-plugin-facebook-auth/)
|
||||
@@ -72,7 +77,8 @@ permalink: /pre-launch-checklist/
|
||||
- See [our blog for more information](https://coralproject.net/blog/slacking-on/)
|
||||
|
||||
|
||||
- [ ] Has your community team configured Talk to match your community strategy?
|
||||
- [ ] Have you configured Talk’s admin settings and determined your community strategy?
|
||||
- See [our tutorial for more information](https://docs.coralproject.net/talk/when-youve-installed-talk/)
|
||||
- See [Configuring Talk](/talk/configuring-talk/)
|
||||
|
||||
|
||||
|
||||
@@ -416,13 +416,19 @@ cannot. You would use this option in the latter situation.
|
||||
|
||||
## TALK_SMTP_FROM_ADDRESS
|
||||
|
||||
The email address to send emails from using the SMTP provider in the format:
|
||||
The email address to send emails from using the SMTP provider. You can include the name and email address or only provide the email.
|
||||
|
||||
```plain
|
||||
TALK_SMTP_FROM_ADDRESS="The Coral Project" <support@coralproject.net>
|
||||
TALK_SMTP_FROM_ADDRESS="The Coral Project" support@coralproject.net
|
||||
|
||||
```
|
||||
or
|
||||
|
||||
```plain
|
||||
TALK_SMTP_FROM_ADDRESS=support@coralproject.net
|
||||
|
||||
```
|
||||
|
||||
Including the name and email address.
|
||||
|
||||
## TALK_SMTP_HOST
|
||||
|
||||
@@ -499,8 +505,9 @@ Could be read as:
|
||||
pre-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.
|
||||
- At the moment of writing, behavior is not attached to the flagging
|
||||
reliability, but it is recorded.
|
||||
- At the moment of writing, behavior is not attached to reliable commenters
|
||||
as well as the flagging reliability and unreliability. Regardless,
|
||||
all of these scores are recorded for future implementation.
|
||||
|
||||
## TALK_DISABLE_IGNORE_FLAGS_AGAINST_STAFF
|
||||
|
||||
|
||||
@@ -81,12 +81,17 @@ Usage: cli-assets [options] [command]
|
||||
Commands:
|
||||
|
||||
list [options] list all the assets in the database
|
||||
debug <url> prints the scraped metadata from that URL
|
||||
refresh [age] queues the assets that exceed the age requested
|
||||
update-url <assetID> <url> update the URL of an asset
|
||||
merge <srcID> <dstID> merges two assets together by moving comments from src to dst and deleting the src asset
|
||||
rewrite [options] <search> <replace> rewrites asset url's using the provided regex replacement pattern
|
||||
```
|
||||
|
||||
When using the `refresh` command, the `age` value specifies how far back in time to re-scrape assets; i.e. to re-scrape everything that was scraped in the last week use `1w` or `7d`. Supports ms (milliseconds), s (seconds), m (minutes), h (hours), d (days) and w (weeks). Assets that have not been scraped will also be queued for scraping.
|
||||
|
||||
See also, [Asset Scraping](/talk/integrating/asset-scraping/) for more details about asset scraping.
|
||||
|
||||
## Setting up the application
|
||||
You can also run a setup wizard to setup the wizard using `./bin/cli setup`. Below is a list of additional options available for this command:
|
||||
```
|
||||
|
||||
@@ -57,7 +57,7 @@ The timeframe in seconds in which commenters have to edit their comment.
|
||||
|
||||
#### Close Comments After
|
||||
|
||||
Default time after which all comment streams will close.
|
||||
Default time after which all comment streams will close. Applies to assets created in Talk after this configuration is saved, and does not update existing assets.
|
||||
|
||||
### Moderation Settings
|
||||
|
||||
@@ -88,8 +88,9 @@ Comments with suspect words will display until a moderator takes action on them.
|
||||
|
||||
#### Permitted Domains
|
||||
|
||||
A list of domains where your Talk instance is allowed to be embedded. Typical
|
||||
use is `localhost`, `staging.yourdomain.com`, `yourdomain.com`, etc.
|
||||
A list of domains where your Talk instance is allowed to be embedded. List each domain and subdomain in use
|
||||
separately, for example: `localhost`, `staging.yourdomain.com`, `yourdomain.com`, `www.yourdomain.com`, etc.
|
||||
|
||||
|
||||
#### Embed Script
|
||||
|
||||
|
||||
@@ -47,3 +47,9 @@ Source: [talk-recipes/tree/master/plugins/author-name](https://github.com/coralp
|
||||
Enables the ability to hover over a commenter’s name and add plugin
|
||||
functionality there. The Member Since plugin that is provided in this recipe is
|
||||
an example of this.
|
||||
|
||||
## recipe-custom-auth
|
||||
|
||||
Source: [talk-recipes/tree/master/plugins/custom-auth](https://github.com/coralproject/talk-recipes/tree/master/plugins/custom-auth)
|
||||
|
||||
Example code for creating, and updating Users in Talk from JWT data provided by your custom SSO authentication service.
|
||||
|
||||
@@ -87,6 +87,8 @@ The most important predictors of the success of an online community are:
|
||||
|
||||
#### Effectively
|
||||
|
||||
* Keep conversations fresh and reduce moderation overhead by configuring Talk to automatically close commenting after a specified time window. Should comments be open for a day, a week, or a month? Set a manageable window and automatically close stories for commenting after that time period.
|
||||
|
||||
* If you're using the Toxic Comments plugin, make sure that its threshold is set at the level that catches most comments with fewest false positives (default is 80%). You can see the Likely to be Toxic level of every comment by clicking "More Details" on the comment card in the moderation view.
|
||||
|
||||
* Publicly discourage behavior in the comments that doesn't cross the line but suggests that the tone or focus could shift quickly in a direction you don't want. Point to relevant sections of your community guidelines. [Read more about defining and discouraging this kind of behavior here.](https://guides.coralproject.net/manage-a-successful-community/)
|
||||
|
||||
+245
-168
@@ -56,16 +56,32 @@ Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
|
||||
definitions. `enum`'s will be appended to, types will be appended, and new types
|
||||
will be added.
|
||||
|
||||
This can be included as a plugin via:
|
||||
|
||||
```js
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
typeDefs: fs.readFileSync(path.join(__dirname, "typeDefs.graphql"), "utf8")
|
||||
};
|
||||
```
|
||||
|
||||
Assuming the above GraphQL schema is located in the plugin folder at
|
||||
`typeDefs.graphql`.
|
||||
|
||||
### context
|
||||
|
||||
```js
|
||||
{
|
||||
Slack: (context) => ({
|
||||
notify: (message) => {
|
||||
// return a promise after we're done sending notifications.
|
||||
}
|
||||
})
|
||||
}
|
||||
module.exports = {
|
||||
context: {
|
||||
Slack: context => ({
|
||||
notify: message => {
|
||||
// return a promise after we're done sending notifications.
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Any property provided here will be added to the context parameter available
|
||||
@@ -83,41 +99,50 @@ A special context hook, `Sort` will allow plugin authors to provide new
|
||||
methods to sort data. An example is as follows:
|
||||
|
||||
```js
|
||||
{
|
||||
Sort: () => ({
|
||||
Comments: { // <-- (1)
|
||||
likes: { // <-- (2)
|
||||
startCursor(ctx, nodes, {cursor}) { // <-- (3)
|
||||
return cursor != null ? cursor : 0;
|
||||
},
|
||||
endCursor(ctx, nodes, {cursor}) { // <-- (4)
|
||||
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
|
||||
},
|
||||
sort(ctx, query, {cursor, sort}) { // <-- (5)
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
module.exports = {
|
||||
context: {
|
||||
Sort: () => ({
|
||||
Comments: {
|
||||
// <-- (1)
|
||||
likes: {
|
||||
// <-- (2)
|
||||
startCursor(ctx, nodes, { cursor }) {
|
||||
// <-- (3)
|
||||
return cursor != null ? cursor : 0;
|
||||
},
|
||||
endCursor(ctx, nodes, { cursor }) {
|
||||
// <-- (4)
|
||||
return nodes.length
|
||||
? (cursor != null ? cursor : 0) + nodes.length
|
||||
: null;
|
||||
},
|
||||
sort(ctx, query, { cursor, sort }) {
|
||||
// <-- (5)
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
|
||||
return query.sort({
|
||||
'action_counts.like': sort === 'DESC' ? -1 : 1,
|
||||
created_at: sort === 'DESC' ? -1 : 1,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
return query.sort({
|
||||
"action_counts.like": sort === "DESC" ? -1 : 1,
|
||||
created_at: sort === "DESC" ? -1 : 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
This has a bunch of special features:
|
||||
|
||||
1. `Comments` is the name of the type being sorted, this is pluralized and
|
||||
capitalized.
|
||||
capitalized.
|
||||
2. `likes` is the `sortBy` field in lowercase.
|
||||
3. `startCursor` will retrieve the start cursor based on the current set of
|
||||
nodes and the current cursor.
|
||||
nodes and the current cursor.
|
||||
4. `endCursor` will retrieve the end cursor based on the current set of nodes
|
||||
and the current cursor.
|
||||
and the current cursor.
|
||||
5. `sort` will mutate the `query` to apply the sort operations.
|
||||
|
||||
All the `startCursor`, `endCursor`, and `sort` functions must be provided in
|
||||
@@ -126,11 +151,13 @@ order for the sorting to apply properly.
|
||||
### loaders
|
||||
|
||||
```js
|
||||
(context) => ({
|
||||
People: {
|
||||
load: () => db.people.find({user: context.user})
|
||||
}
|
||||
})
|
||||
module.exports = {
|
||||
context: context => ({
|
||||
People: {
|
||||
load: () => db.people.find({ user: context.user })
|
||||
}
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
Loaders should be provided as a function which returns a map which is used in
|
||||
@@ -139,13 +166,15 @@ the resolvers function. These must return a promise or a value.
|
||||
### mutators
|
||||
|
||||
```js
|
||||
(context) => ({
|
||||
People: {
|
||||
create: (name) => {
|
||||
return db.people.insert({user: context.user, name});
|
||||
module.exports = {
|
||||
context: context => ({
|
||||
People: {
|
||||
create: name => {
|
||||
return db.people.insert({ user: context.user, name });
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
Mutators should be provided as a function which returns a map which is used in
|
||||
@@ -154,27 +183,41 @@ the resolvers function. These must return a promise or a value.
|
||||
### resolvers
|
||||
|
||||
```js
|
||||
{
|
||||
Person: {
|
||||
name(obj, args, context) {
|
||||
return obj.name;
|
||||
module.exports = {
|
||||
resolvers: {
|
||||
Person: {
|
||||
name(obj, args, context) {
|
||||
return obj.name;
|
||||
},
|
||||
colour(obj, args, context) {
|
||||
// Bill likes the colour red, everyone else likes blue.
|
||||
return obj.name === "bill" ? "RED" : "BLUE";
|
||||
}
|
||||
},
|
||||
colour(obj, args, context) {
|
||||
// Bill likes the colour red, everyone else likes blue.
|
||||
return obj.name === 'bill' ? 'RED' : 'BLUE';
|
||||
}
|
||||
},
|
||||
RootQuery: {
|
||||
people(obj, args, {loaders: {People}}) {
|
||||
return People.load();
|
||||
}
|
||||
},
|
||||
RootMutation: {
|
||||
createPerson(obj, {name}, {mutators: {People}}) {
|
||||
return People.create(name);
|
||||
RootQuery: {
|
||||
people(
|
||||
obj,
|
||||
args,
|
||||
{
|
||||
loaders: { People }
|
||||
}
|
||||
) {
|
||||
return People.load();
|
||||
}
|
||||
},
|
||||
RootMutation: {
|
||||
createPerson(
|
||||
obj,
|
||||
{ name },
|
||||
{
|
||||
mutators: { People }
|
||||
}
|
||||
) {
|
||||
return People.create(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Should return a resolver map as described in the
|
||||
@@ -185,40 +228,44 @@ This will merge with the existing resolvers in core and from previous plugins.
|
||||
### hooks
|
||||
|
||||
```js
|
||||
{
|
||||
RootMutation: {
|
||||
createPerson: {
|
||||
post: async (obj, args, {plugins: {Slack}}, info, person) {
|
||||
if (!person) {
|
||||
module.exports = {
|
||||
hooks: {
|
||||
RootMutation: {
|
||||
createPerson: {
|
||||
post: async (obj, args, {plugins: {Slack}}, info, person) {
|
||||
if (!person) {
|
||||
return person;
|
||||
}
|
||||
|
||||
await Slack.notify(`A new person just was created with name ${person.name}`);
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
await Slack.notify(`A new person just was created with name ${person.name}`);
|
||||
|
||||
return person;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Hooks here are pretty special, for each resolver field, you can specify a
|
||||
pre/post hook that will execute pre and post field resolution.
|
||||
|
||||
If your post function accepts four parameters, then it can modify the field
|
||||
result. It is *required* that the function resolves a promise (or returns) with
|
||||
result. It is _required_ that the function resolves a promise (or returns) with
|
||||
the modified value or simply the original if you didn't modify it.
|
||||
|
||||
### setupFunctions
|
||||
|
||||
```js
|
||||
setupFunctions: {
|
||||
leader: (options, args) => ({
|
||||
leader: {
|
||||
filter: (person) => person.place === 1
|
||||
},
|
||||
}),
|
||||
}
|
||||
module.exports = {
|
||||
setupFunctions: {
|
||||
leader: (options, args) => ({
|
||||
leader: {
|
||||
filter: person => person.place === 1
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Setup functions allow you to create filters that control which pubsub.publish() events
|
||||
@@ -229,28 +276,32 @@ For more information, see the [Apollo Docs](https://github.com/apollographql/gra
|
||||
### tokenUserNotFound
|
||||
|
||||
```js
|
||||
tokenUserNotFound: async ({jwt, token}) => {
|
||||
let profile = await someExternalService(token);
|
||||
if (!profile) {
|
||||
return null;
|
||||
const Users = require("services/users");
|
||||
|
||||
module.exports = {
|
||||
tokenUserNotFound: async ({ jwt }) => {
|
||||
const user = await Users.upsertExternalUser(
|
||||
null,
|
||||
jwt.sub,
|
||||
jwt.iss,
|
||||
jwt.username
|
||||
);
|
||||
|
||||
const email = jwt.email.toLowerCase();
|
||||
|
||||
//upsertExternalUser will also create a profile with provider:jwt.iss, id:jwt.sub
|
||||
//add a "local" profile to persist email on the user
|
||||
user.profiles.push({
|
||||
provider: "local",
|
||||
id: email
|
||||
});
|
||||
user.created_at = new Date(jwt.memberSince * 1000);
|
||||
|
||||
await user.save();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
let user = await UserModel.findOneAndUpdate({
|
||||
id: profile.id
|
||||
}, {
|
||||
id: profile.id,
|
||||
username: profile.username,
|
||||
lowercaseUsername: profile.username.toLowerCase(),
|
||||
roles: [],
|
||||
profiles: []
|
||||
}, {
|
||||
setDefaultsOnInsert: true,
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The `tokenUserNotFound` hook allows auth integrations to hook into the event
|
||||
@@ -258,8 +309,15 @@ when a valid token is provided but a user can't be found in the database that
|
||||
matches the provided id.
|
||||
|
||||
The function is async, and should return the user object that was created in the
|
||||
database, or null if the user wasn't found. The `jwt` parameter of the object
|
||||
is the unpacked token, while `token` is the original jwt token string.
|
||||
database. The `jwt` parameter of the object is the unpacked token, which has already been validated.
|
||||
|
||||
More details about the `upsertExternalUser` method can be found at:
|
||||
https://github.com/coralproject/talk/blob/32962aa1e84c09d87141440a1f04cbd1659b3336/services/users.js/#L592
|
||||
|
||||
In this example, a unix timestamp was included on the jwt with a date that is being persisted on the user. This is optional, but can be used to handle any extra claims you want to include on the jwt. You can use the metadata object on the user to store custom values.
|
||||
|
||||
Having trouble implementing your own `tokenUserNotFound` hook?
|
||||
Submit a Support ticket ([support@coralproject.net](mailto:support@coralproject.net))
|
||||
|
||||
### tags
|
||||
|
||||
@@ -268,18 +326,20 @@ or enabled by code). Below is an example pulled from the core off topic plugin
|
||||
on how to create a hook for the `OFF_TOPIC` name:
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
name: 'OFF_TOPIC',
|
||||
permissions: {
|
||||
public: true,
|
||||
self: true,
|
||||
roles: []
|
||||
},
|
||||
models: ['COMMENTS'],
|
||||
created_at: new Date()
|
||||
}
|
||||
]
|
||||
module.exports = {
|
||||
tags: [
|
||||
{
|
||||
name: "OFF_TOPIC",
|
||||
permissions: {
|
||||
public: true,
|
||||
self: true,
|
||||
roles: []
|
||||
},
|
||||
models: ["COMMENTS"],
|
||||
created_at: new Date()
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
You can refer to `models/schema/tag.js` for the available schema to match when
|
||||
@@ -288,11 +348,13 @@ creating models to enable/disable specific features.
|
||||
### router
|
||||
|
||||
```js
|
||||
(router) => {
|
||||
router.get('/api/v1/people', (req, res) => {
|
||||
res.json({people: [{name: 'Bob'}]});
|
||||
});
|
||||
}
|
||||
module.exports = {
|
||||
router: router => {
|
||||
router.get("/api/v1/people", (req, res) => {
|
||||
res.json({ people: [{ name: "Bob" }] });
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The Router hook allows you to create a function that accepts the base express
|
||||
@@ -302,56 +364,73 @@ action needed by external applications.
|
||||
### passport
|
||||
|
||||
```js
|
||||
const FacebookStrategy = require('passport-facebook').Strategy;
|
||||
const UsersService = require('services/users');
|
||||
const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
|
||||
const FacebookStrategy = require("passport-facebook").Strategy;
|
||||
const UsersService = require("services/users");
|
||||
const {
|
||||
ValidateUserLogin,
|
||||
HandleAuthPopupCallback
|
||||
} = require("services/passport");
|
||||
|
||||
module.exports = {
|
||||
passport(passport) {
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: process.env.TALK_FACEBOOK_APP_ID,
|
||||
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
|
||||
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
|
||||
passReqToCallback: true,
|
||||
profileFields: ['id', 'displayName', 'picture.type(large)']
|
||||
}, async (req, accessToken, refreshToken, profile, done) => {
|
||||
passport: passport => {
|
||||
passport.use(
|
||||
new FacebookStrategy(
|
||||
{
|
||||
clientID: process.env.TALK_FACEBOOK_APP_ID,
|
||||
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
|
||||
callbackURL: `${
|
||||
process.env.TALK_ROOT_URL
|
||||
}/api/v1/auth/facebook/callback`,
|
||||
passReqToCallback: true,
|
||||
profileFields: ["id", "displayName", "picture.type(large)"]
|
||||
},
|
||||
async (req, accessToken, refreshToken, profile, done) => {
|
||||
let user;
|
||||
try {
|
||||
const { id, provider, displayName } = profile;
|
||||
user = await UsersService.upsertSocialUser(
|
||||
req.context,
|
||||
id,
|
||||
provider,
|
||||
displayName
|
||||
);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
let user;
|
||||
try {
|
||||
const { id, provider, displayName } = profile;
|
||||
user = await UsersService.upsertSocialUser(
|
||||
req.context,
|
||||
id,
|
||||
provider,
|
||||
displayName
|
||||
);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return ValidateUserLogin(profile, user, done);
|
||||
}));
|
||||
return ValidateUserLogin(profile, user, done);
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
router(router) {
|
||||
|
||||
router: router => {
|
||||
// Note that we have to import the passport instance here, it is
|
||||
// instantiated after all the strategies have been mounted.
|
||||
const {passport} = require('services/passport');
|
||||
const { passport } = require("services/passport");
|
||||
|
||||
/**
|
||||
* Facebook auth endpoint, this will redirect the user immediately to facebook
|
||||
* for authorization.
|
||||
*/
|
||||
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
|
||||
router.get(
|
||||
"/facebook",
|
||||
passport.authenticate("facebook", {
|
||||
display: "popup",
|
||||
authType: "rerequest",
|
||||
scope: ["public_profile"]
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Facebook callback endpoint, this will send the user a html page designed to
|
||||
* send back the user credentials upon successful login.
|
||||
*/
|
||||
router.get('/facebook/callback', (req, res, next) => {
|
||||
|
||||
router.get("/facebook/callback", (req, res, next) => {
|
||||
// Perform the facebook login flow and pass the data back through the opener.
|
||||
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
|
||||
passport.authenticate(
|
||||
"facebook",
|
||||
HandleAuthPopupCallback(req, res, next)
|
||||
)(req, res, next);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -360,10 +439,10 @@ module.exports = {
|
||||
### translations
|
||||
|
||||
```js
|
||||
const path = require('path');
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
translations: path.join(__dirname, 'translations.yml'),
|
||||
translations: path.join(__dirname, "translations.yml")
|
||||
};
|
||||
```
|
||||
|
||||
@@ -377,11 +456,11 @@ en:
|
||||
|
||||
Which overrides the copy for the `embedlink.copy` template. You can
|
||||
also provide other languages as well by using the correct language
|
||||
prefix.
|
||||
prefix.
|
||||
|
||||
When creating a plugin using this `translations` hook to override copy
|
||||
When creating a plugin using this `translations` hook to override copy
|
||||
from another plugin, be sure to list it after the plugin it's overriding
|
||||
in the `plugins.json` file.
|
||||
in the `plugins.json` file.
|
||||
|
||||
### websockets
|
||||
|
||||
@@ -392,12 +471,12 @@ module.exports = {
|
||||
// Do something with the connection params or connection, like
|
||||
// logging it out, or incrementing a metric.
|
||||
},
|
||||
onDisconnect: (connection) => {
|
||||
onDisconnect: connection => {
|
||||
// Do something with the connection params or connection, like
|
||||
// logging it out, or decrementing a metric.
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
This `websockets` hook can be used to attach methods to the
|
||||
@@ -411,12 +490,13 @@ connections.
|
||||
module.exports = {
|
||||
schemaLevelResolveFunction: (root, args, ctx, info) => {
|
||||
// The GraphQL Operation Name. Example: CoralEmbedStream_Embed
|
||||
const name = info.operation.name !== null ? info.operation.name.value : null;
|
||||
const name =
|
||||
info.operation.name !== null ? info.operation.name.value : null;
|
||||
// Maybe increment a metric based on the operation name...
|
||||
|
||||
// You must _always_ return the root.
|
||||
return root;
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -431,9 +511,7 @@ Contents of `plugins.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": [
|
||||
"people"
|
||||
]
|
||||
"server": ["people"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -527,5 +605,4 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Accelerated Mobile Page
|
||||
permalink: /integrating/amp/
|
||||
---
|
||||
|
||||
[AMP](https://amp.dev/) is a light-weight, stripped down HTML page that aims to improve reader experience. _Talk v4.9.0+_ comes with [AMP](https://amp.dev/) support. The current caveat however is that _toast notifications_ are not being rendered when viewing inside AMP.
|
||||
|
||||
# How to integrate
|
||||
Put the following code into your _AMP_ page and replace `$TALK_URL` and `$ASSET_URL` with the
|
||||
corresponding values. You can also pass `asset_id` instead of `asset_url`.
|
||||
|
||||
```html
|
||||
<amp-iframe
|
||||
width=600 height=140
|
||||
layout="responsive"
|
||||
sandbox="allow-scripts allow-same-origin allow-modals allow-popups allow-forms"
|
||||
resizable
|
||||
src="https://$TALK_URL/embed/amp#asset_url=$ASSET_URL">
|
||||
<div placeholder></div>
|
||||
<div overflow tabindex=0 role=button aria-label="Read more">Read more</div>
|
||||
</amp-iframe>
|
||||
```
|
||||
|
||||
## Single Sign-On
|
||||
For SSO integration you need to create a page with the following output and replace `$TALK_URL` and `$AUTH_TOKEN` with the appropriate values. Inject your SSO auth scripts to get the `$AUTH_TOKEN` for the current user. Integrating with [amp-access](https://amp.dev/documentation/components/amp-access) is recommended which opens a 1st-party popup to not have browsers block your cookies. This page is then used in `src` of `<amp-iframe>` above. It must be accessed over `https` and live in a different domain than the `amp` page.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<title>Coral Talk Amp Embed</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script src="https://$TALK_URL/static/embed.js"></script>
|
||||
<script>
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '$TALK_URL',
|
||||
auth_token: '$AUTH_TOKEN',
|
||||
amp: true,
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -9,6 +9,9 @@ in a simple way. We use the following
|
||||
[meta tags](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) on
|
||||
the target pages that allow us to extract some properties.
|
||||
|
||||
Asset scraping is performed by the `scraper` job which is enabled by default when you launch Talk. If your production site is behind a paywall or otherwise prevents scraping, you might need to confiugre a [TALK_SCRAPER_PROXY_URL](/talk/advanced-configuration/#talk-scraper-proxy-url) or custom [TALK_SCRAPER_HEADERS](/talk/advanced-configuration/#talk-scraper-headers).
|
||||
|
||||
|
||||
| Asset Property | Selector |
|
||||
|--------------------|----------|
|
||||
| `title` | See [`metascraper-title`](https://github.com/microlinkhq/metascraper/blob/dc664c37ea1b238b1e3e9d5342edfacc9027892c/packages/metascraper-title/index.js) |
|
||||
@@ -19,7 +22,7 @@ the target pages that allow us to extract some properties.
|
||||
| `modified_date` | `meta[property="article:modified"]` |
|
||||
| `section` | `meta[property="article:section"]` |
|
||||
|
||||
You can use the `./bin/cli assets debug <url>` to print the scraped metadata
|
||||
You can use the `./bin/cli assets debug <url>` command to print the scraped metadata
|
||||
from that URL. For example:
|
||||
|
||||
```bash
|
||||
@@ -41,4 +44,8 @@ from that URL. For example:
|
||||
├──────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ section │ │
|
||||
└──────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
|
||||
You can use the `./bin/cli assets refresh [age]` to trigger scraping or rescrape assets where the scraper job was unsuccessful.
|
||||
@@ -3,25 +3,18 @@ title: Authenticating with Talk
|
||||
permalink: /integrating/authentication/
|
||||
---
|
||||
|
||||
Out of the box Talk supports account registration with username and password, as well as features like forgot password.
|
||||
### Authentication Strategies
|
||||
|
||||
You can also integrate Talk with any external authentication service that will enable
|
||||
seamless single sign-on (SSO) for users within your organization. There are a few
|
||||
methods of doing so:
|
||||
Talk comes with built in user management features like account registration with username/email and password, as well as features like resetting a forgotten password.
|
||||
|
||||
1. Passport Middleware
|
||||
2. Custom Token Integration
|
||||
There are four strategies to choose from when it comes to user authentication:
|
||||
|
||||
Both methods work, but there are product decisions that will affect the overall
|
||||
choice.
|
||||
#### Talk Email Authentication
|
||||
Best when you want Talk to manage all user registration, creation and authentication. Requires the least amount of setup and configuration. Users register by creating a Username (must be unique across users, no spaces or special chars), Email, and Password and are stored in Talk’s database. Optionally - you can require an email verification step at time of account creation. *Plugins required: [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth), [talk-plugin-auth](/talk/plugin/talk-plugin-auth)*
|
||||
|
||||
## Passport Middleware
|
||||
#### Social Sign-in (FaceBook or Google via Passport Middleware)
|
||||
|
||||
You would choose the **Passport Middleware** route when you are OK using an auth
|
||||
that is triggered from inside Talk that is not connected to an external auth
|
||||
state (you don't use the auth anywhere else now).
|
||||
|
||||
Plugins are available for the following 3rd party authentication providers:
|
||||
Allows users to use existing social media account to sign in to Talk. Requires you to setup API keys with social sign on provider, and configure your API keys in Talk. Users register and login via a *“Sign in with ….”*, but are not required to set user details such as password or email address. *Plugins required: [talk-plugin-auth](/talk/plugin/talk-plugin-auth), plus at least one of the following:*
|
||||
|
||||
* [Facebook](/talk/plugin/talk-plugin-facebook-auth/)
|
||||
* [Google](/talk/plugin/talk-plugin-google-auth/)
|
||||
@@ -29,15 +22,20 @@ Plugins are available for the following 3rd party authentication providers:
|
||||
_FAQ: Can I create a Twitter auth plugin?_
|
||||
This is currently not possible because Talk uses passport.js which does not support Twitter's oAuth2 requirements.
|
||||
|
||||
## Custom Token Integration
|
||||
#### Social Sign-in with Email Association
|
||||
This strategy is actually a combination of the first two. Talk’s built in Email Authentication is augmented by also allowing registration with a social account. Users with social accounts are prompted to associate an email address with their social profile to enable notifications. *Plugins required: [talk-plugin-auth](/talk/plugin/talk-plugin-auth), [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth), [talk-plugin-facebook-auth](/talk/plugin/talk-plugin-facebook-auth/), and or [talk-plugin-google-auth](/talk/plugin/talk-plugin-google-auth/)*
|
||||
|
||||
#### Custom SSO Token Integration
|
||||
When you want users to authenticate via your existing identity system, and be automatically registered/logged into Talk. Users register, login, and manage their user details on your existing site, your user database maintains the source of truth for all user information. Users are created and updated in Talk via jwt tokens passed from your auth service to Talk. *Required Plugins: requires custom plugin development, as well as disabling talk-plugin-auth, and talk-plugin-local-auth*
|
||||
|
||||
## Setting Up Custom SSO Token Integration
|
||||
|
||||
You can integrate Talk with any authentication service to enable single sign-on
|
||||
for users. The steps to do that are:
|
||||
|
||||
1. Create a service that generates [JWT tokens](https://jwt.io/introduction/).
|
||||
2. Push the token into the embed.
|
||||
3. Implement the [`tokenUserNotFound`](#implement-tokenusernotfound) hook to
|
||||
process the token.
|
||||
2. Include the JWT token in your Talk embed script
|
||||
3. Create a custom plugin to generate and authenticate Talk users from the jwt token
|
||||
|
||||
### Create JWT Token
|
||||
|
||||
@@ -67,7 +65,7 @@ The generated JWT must contain the following claims:
|
||||
### Generate a key to sign the JWT
|
||||
Optionally you can use https://github.com/coralproject/coralcert to generate a key with which to sign the JWTs and specify the secret as an environment variable.
|
||||
|
||||
### Push token into embed
|
||||
### Include token in embed
|
||||
|
||||
We're assuming that your CMS is capable of authenticating a user account, or
|
||||
at least having the user's details available to send off to the token creation
|
||||
@@ -83,15 +81,17 @@ Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
});
|
||||
```
|
||||
|
||||
Which will pass down the token to Talk and will fire the next steps
|
||||
Which will pass down the token to Talk and will fire the
|
||||
`tokenUserNotFound` hook to complete the auth flow.
|
||||
|
||||
### Implement `tokenUserNotFound`
|
||||
### Create a Custom Plugin to Generate Talk Users
|
||||
|
||||
This is the only piece of code you'll have to write that lives inside Talk.
|
||||
The role of this code is to live as a plugin and provide Talk with a way of
|
||||
taking the token that you gave it, and turning into a user.
|
||||
|
||||
Your custom plugin will need to include a `tokenUserNotFound` function that will handle creating a user in Talk from the claims provided on the JWT, refer to [`tokenUserNotFound`](/talk/api/server/#tokenusernotfound) for reference.
|
||||
|
||||
Using the example application we were working with in the JWT issuing step
|
||||
above, we'll need to ensure that the configuration is consistent in-between both
|
||||
Talk and the JWT issuer. Namely, the following environment variables from our
|
||||
@@ -108,5 +108,4 @@ example issuer and Talk must match:
|
||||
reference, the basic takeaway is that the secret used to sign the tokens issued
|
||||
by the issuer must be able to be verified by Talk.
|
||||
|
||||
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/api/server/#tokenusernotfound)
|
||||
reference.
|
||||
For an example of implementing the plugin, check out our [SSO Custom Auth Recipe](https://github.com/coralproject/talk-recipes/tree/master/plugins/custom-auth)
|
||||
|
||||
@@ -4,47 +4,87 @@ permalink: /integrating/cms-integration/
|
||||
---
|
||||
|
||||
## Embedding Comments on Your Site
|
||||
|
||||
Talk provides an embed script that you can drop into your site where you want a comments section to appear. By default that script dynamically generate Assets
|
||||
in Talk in order to make it easier for lighter installations.
|
||||
|
||||
You can find the embed script inside talk under `Configure > Tech Settings > Embed Script`. It should look something like this, but with your domain in place of `<TALK_ROOT_URL>`:
|
||||
```
|
||||
You can find the embed script inside talk under `Configure > Tech Settings > Embed Script`. It should look something like this, but with your domain in place of `${TALK_ROOT_URL}`:
|
||||
|
||||
```html
|
||||
<div id="coral_talk_stream"></div>
|
||||
<script src="<TALK_ROOT_URL>/static/embed.js" async onload="
|
||||
<script
|
||||
src="${TALK_ROOT_URL}/static/embed.js"
|
||||
async
|
||||
onload="
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '<TALK_ROOT_URL>'
|
||||
talk: '${TALK_ROOT_URL}'
|
||||
});
|
||||
"></script>
|
||||
"
|
||||
></script>
|
||||
```
|
||||
|
||||
The URL for the asset is first inferred from the _Canonical link element_, which
|
||||
takes the form of a `<link>` element in your `<head>` of the page:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="canonical" href="https://example.com/page" />
|
||||
</head>
|
||||
<body>
|
||||
...
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
If this tag is not present, you can also pass a `asset_url` parameter into the
|
||||
render function as:
|
||||
|
||||
```js
|
||||
Coral.Talk.render(document.getElementById("coral_talk_stream"), {
|
||||
talk: "${TALK_ROOT_URL}",
|
||||
asset_url: "https://example.com/page"
|
||||
});
|
||||
```
|
||||
|
||||
Which will explicitly force Talk to reference a particular url. This is
|
||||
recommended if your canonical url does not match the current url.
|
||||
|
||||
> **NOTE:** If the canonical link tag or `asset_url` parameter are not present, Talk will use the current URL excluding the query and hash elements. This may lead to undesired behavior, and it is recommended to use one of the above methods of specifying the URL.
|
||||
|
||||
## Triggering the Comments Section (client side, i.e. Your site)
|
||||
|
||||
When the embed script is triggered on your page load several things are initiated inside Talk, including fetching all comments for the specified article, establishing websocket connections for this user, and checking user’s session for SSO/authentication.
|
||||
When the embed script is triggered on your page load several things are initiated inside Talk, including fetching all comments for the specified article, establishing websocket connections for this user, and checking user’s session for SSO/authentication.
|
||||
|
||||
Instead of greedily triggering the embed to render on _EVERY PAGE LOAD_, we highly recommend implementing a _“lazy”_ rendering strategy to only render the comments section if a user wants to interact with it. This will greatly improve your initial page load performance, and will be critical to managing server resources if you’re running Talk on a heavy-traffic production site.
|
||||
Instead of greedily triggering the embed to render on _EVERY PAGE LOAD_, we highly recommend implementing a _“lazy”_ rendering strategy to only render the comments section if a user wants to interact with it. This will greatly improve your initial page load performance, and will be critical to managing server resources if you’re running Talk on a heavy-traffic production site.
|
||||
|
||||
We recommend using one of these _“lazy”_ loading strategies:
|
||||
|
||||
#### Scroll to Comments Section
|
||||
Wait for user to scroll to the comment section before triggering the embed to render.
|
||||
|
||||
Wait for user to scroll to the comment section before triggering the embed to render.
|
||||
|
||||
You can pass lazy: true to the render options, like so:
|
||||
```
|
||||
Coral.Talk.render(document.getElementById('container'), {
|
||||
talk: 'https://my-talk-installation.com',
|
||||
lazy: true,
|
||||
|
||||
```js
|
||||
Coral.Talk.render(document.getElementById("container"), {
|
||||
talk: "${TALK_ROOT_URL}",
|
||||
lazy: true
|
||||
});
|
||||
```
|
||||
|
||||
Or you can enable lazy rendering by default on all assets using ENV variable `TALK_DEFAULT_LAZY_RENDER=TRUE`
|
||||
|
||||
_*Note: This feature requires Talk version 4.6.8 or greater_
|
||||
_\*Note: This feature requires Talk version 4.6.8 or greater_
|
||||
|
||||
#### Show Comments Button
|
||||
You can hide the comments section until a user clicks button, then trigger the embed to render
|
||||
|
||||
You can hide the comments section until a user clicks button, then trigger the embed to render
|
||||
|
||||
This example uses jQuery to render the embed on the button's click event
|
||||
```
|
||||
|
||||
```html
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||
|
||||
<div id="coral_talk_stream">
|
||||
@@ -52,10 +92,10 @@ This example uses jQuery to render the embed on the button's click event
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('#coral_talk_stream button').on('click', function() {
|
||||
$.getScript('<TALK_ROOT_URL>/static/embed.js', function() {
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '<TALK_ROOT_URL>',
|
||||
$("#coral_talk_stream button").on("click", function() {
|
||||
$.getScript("${TALK_ROOT_URL}/static/embed.js", function() {
|
||||
Coral.Talk.render(document.getElementById("coral_talk_stream"), {
|
||||
talk: "${TALK_ROOT_URL}"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -63,17 +103,20 @@ This example uses jQuery to render the embed on the button's click event
|
||||
```
|
||||
|
||||
## Creating Assets in Talk (server side, i.e. What you need to send to Talk)
|
||||
|
||||
One of the most frequent questions that we get asked by organizations trying to
|
||||
integrate Talk is: _How do we hook our CMS up to Talk so that articles are in
|
||||
sync?_
|
||||
|
||||
### “Lazy Asset Creation”
|
||||
Talk’s provided embed script by default dynamically generates Assets in Talk based on each unique url that triggers it. The url must reference an existing Permitted Domain. If your articles/stories always have unique urls, then you will not need to modify the default behavior.
|
||||
|
||||
Talk’s provided embed script by default dynamically generates Assets in Talk based on each unique url that triggers it. The url must reference an existing Permitted Domain. If your articles/stories always have unique urls, then you will not need to modify the default behavior.
|
||||
|
||||
Assets created in this way will then be scraped to load metadata, see [Asset Scraping](/talk/integrating/asset-scraping/)
|
||||
|
||||
## Customizing the Integration with a Plugin
|
||||
In order to have more strict control over the asset creation flow to allow only assets pushed into it from your CMS, and keep your URL/title in sync we will create a plugin.
|
||||
|
||||
In order to have more strict control over the asset creation flow to allow only assets pushed into it from your CMS, and keep your URL/title in sync we will create a plugin.
|
||||
|
||||
We will create a plugin that will:
|
||||
|
||||
@@ -83,7 +126,6 @@ We will create a plugin that will:
|
||||
|
||||
We will then modify our embed so that we can [Target the Asset](#target-the-asset).
|
||||
|
||||
|
||||
This guide is designed to explain the steps to take your base installation of Talk and configure it. We won't cover here how to install the plugin, as it is covered in our [Plugins Overview](/talk/plugins/).
|
||||
|
||||
But first we should grab our basic plugin structure:
|
||||
@@ -129,8 +171,8 @@ module.exports = {
|
||||
|
||||
// Send the asset back.
|
||||
return asset;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -158,14 +200,14 @@ We'll replace the contents of the `router.js` file with the following:
|
||||
// This file we'll create routes that will facilitate asset creation and
|
||||
// updates.
|
||||
|
||||
const authz = require('middleware/authorization');
|
||||
const authz = require("middleware/authorization");
|
||||
|
||||
module.exports = router => {
|
||||
// We'll respond to a POST request on the following route where the request
|
||||
// must have a valid ADMIN access token.
|
||||
router.post(
|
||||
'/api/v1/plugin/asset-manager-example',
|
||||
authz.needed('ADMIN'),
|
||||
"/api/v1/plugin/asset-manager-example",
|
||||
authz.needed("ADMIN"),
|
||||
async (req, res, next) => {
|
||||
// Get the graph context from the request.
|
||||
const { context } = req;
|
||||
@@ -173,7 +215,11 @@ module.exports = router => {
|
||||
// Grab from the graph context, the AssetModel that we can use to create
|
||||
// the new Asset. Lots of object destructuring here, but this lets us keep
|
||||
// the important business logic cleaner.
|
||||
const { connectors: { models: { Assets } } } = context;
|
||||
const {
|
||||
connectors: {
|
||||
models: { Assets }
|
||||
}
|
||||
} = context;
|
||||
|
||||
try {
|
||||
// Now we can create the asset that was passed to us in the body of the
|
||||
@@ -208,20 +254,24 @@ CMS using the Talk cli tool:
|
||||
|
||||
You can attach the generated token to the request a few ways:
|
||||
|
||||
1. HTTP Header:
|
||||
1. HTTP Header:
|
||||
|
||||
curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example \
|
||||
-XPOST \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "${ASSET_JSON}"
|
||||
```sh
|
||||
curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example \
|
||||
-XPOST \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "${ASSET_JSON}"
|
||||
```
|
||||
|
||||
2. Query Parameter:
|
||||
2. Query Parameter:
|
||||
|
||||
curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example?access_token=${TOKEN}
|
||||
-XPOST \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "${ASSET_JSON}"
|
||||
```sh
|
||||
curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example?access_token=${TOKEN}
|
||||
-XPOST \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "${ASSET_JSON}"
|
||||
```
|
||||
|
||||
Where `${ASSET_JSON}` is the JSON for your Asset matching the
|
||||
[AssetSchema](https://github.com/coralproject/talk/blob/master/models/asset.js).
|
||||
@@ -239,14 +289,14 @@ Update your `router.js` to the following:
|
||||
// This file we'll create routes that will facilitate asset creation and
|
||||
// updates.
|
||||
|
||||
const authz = require('middleware/authorization');
|
||||
const authz = require("middleware/authorization");
|
||||
|
||||
module.exports = router => {
|
||||
// We'll respond to a POST request on the following route where the request
|
||||
// must have a valid ADMIN access token.
|
||||
router.post(
|
||||
'/api/v1/plugin/asset-manager-example',
|
||||
authz.needed('ADMIN'),
|
||||
"/api/v1/plugin/asset-manager-example",
|
||||
authz.needed("ADMIN"),
|
||||
async (req, res, next) => {
|
||||
// Get the graph context from the request.
|
||||
const { context } = req;
|
||||
@@ -254,7 +304,11 @@ module.exports = router => {
|
||||
// Grab from the graph context, the AssetModel that we can use to create
|
||||
// the new Asset. Lots of object destructuring here, but this lets us keep
|
||||
// the important business logic cleaner.
|
||||
const { connectors: { models: { Assets } } } = context;
|
||||
const {
|
||||
connectors: {
|
||||
models: { Assets }
|
||||
}
|
||||
} = context;
|
||||
|
||||
try {
|
||||
// Now we can create the asset that was passed to us in the body of the
|
||||
@@ -273,8 +327,8 @@ module.exports = router => {
|
||||
// We'll respond to a PUT request on the following route where the request
|
||||
// must also have a valid ADMIN access token.
|
||||
router.put(
|
||||
'/api/v1/plugin/asset-manager-example/:id',
|
||||
authz.needed('ADMIN'),
|
||||
"/api/v1/plugin/asset-manager-example/:id",
|
||||
authz.needed("ADMIN"),
|
||||
async (req, res, next) => {
|
||||
// Get the graph context from the request.
|
||||
const { context } = req;
|
||||
@@ -282,7 +336,11 @@ module.exports = router => {
|
||||
// Grab from the graph context, the AssetModel that we can use to update
|
||||
// the Asset. Lots of object destructuring here, but this lets us keep
|
||||
// the important business logic cleaner.
|
||||
const { connectors: { models: { Assets } } } = context;
|
||||
const {
|
||||
connectors: {
|
||||
models: { Assets }
|
||||
}
|
||||
} = context;
|
||||
|
||||
try {
|
||||
// Now we can lookup the asset we're updating and apply out updates to
|
||||
@@ -292,7 +350,7 @@ module.exports = router => {
|
||||
req.body,
|
||||
{
|
||||
// We want to validate the model being updated.
|
||||
runValidators: true,
|
||||
runValidators: true
|
||||
}
|
||||
);
|
||||
if (!asset) {
|
||||
@@ -343,23 +401,31 @@ When you install Talk, and visit the admin panel, we can see under
|
||||
|
||||
```html
|
||||
<div id="coral_talk_stream"></div>
|
||||
<script src="${TALK_ROOT_URL}static/embed.js" async onload="
|
||||
<script
|
||||
src="${TALK_ROOT_URL}static/embed.js"
|
||||
async
|
||||
onload="
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '${TALK_ROOT_URL}'
|
||||
});
|
||||
"></script>
|
||||
"
|
||||
></script>
|
||||
```
|
||||
|
||||
We'll modify this to the following:
|
||||
|
||||
```html
|
||||
<div id="coral_talk_stream"></div>
|
||||
<script src="${TALK_ROOT_URL}static/embed.js" async onload="
|
||||
<script
|
||||
src="${TALK_ROOT_URL}static/embed.js"
|
||||
async
|
||||
onload="
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '${TALK_ROOT_URL}',
|
||||
asset_id: '${ASSET_ID}'
|
||||
});
|
||||
"></script>
|
||||
"
|
||||
></script>
|
||||
```
|
||||
|
||||
Adding the `asset_id` parameter to the render function will accomplish a very
|
||||
@@ -369,8 +435,10 @@ the URL in the future, the embed will still reference the correct Asset. The
|
||||
`${ASSET_ID}` should be replaced by your CMS with the correct Asset id using
|
||||
your desired scripting/templating tools.
|
||||
|
||||
> **NOTE:** When used in conjunction with `asset_url`, you can explicitly force Talk to use a specified URL, rather than the canonical for link references. When used together, both `asset_id` and `asset_url` will be treated as unique identifiers for Talk assets.
|
||||
|
||||
At this point, you should have a fully built Talk plugin that can be paired with
|
||||
some work on your CMS to create a fully integrated asset management pipeline!
|
||||
|
||||
To view the fully completed source code, visit
|
||||
https://github.com/coralproject/talk-plugin-asset-manager-example.
|
||||
https://github.com/coralproject/talk-plugin-asset-manager-example.
|
||||
|
||||
@@ -3,9 +3,19 @@ title: Tracking Talk Events and Metrics
|
||||
permalink: /integrating/event-tracking-metrics/
|
||||
---
|
||||
|
||||
Talk supports event emitting via Redux, Apollo and GraphQL. This means that common actions taken within Talk, such as successfully posting a comment, posting a reaction, or changing a setting, are automatically emitted from Talk. To send these events to your analytics tool of choice, however, will require some integration on your part.
|
||||
Talk supports event emitting. This means that common actions taken within Talk, such as successfully posting a comment, posting a reaction, or changing a setting, are automatically emitted from Talk. To send these events to your analytics tool of choice, you’ll need to configure your Talk embed to process them.
|
||||
|
||||
First, we want to uncomment the tracking code in `article.ejs` (https://github.com/coralproject/talk/blob/93bda87ad061a2dc5eb8dc5b65a579a20efb76f7/views/article.ejs#L34). This will enable events to be sent via the Talk embed that is on your article pages. This will start a stream of events to the browser console, so that you can see which events are available.
|
||||
The following example demonstrates how the event emitter works on a sample article page generated by Talk. When you’re ready to capture and handle events from your actual site, add the events property along with your event handling functions to your Talk embed script.
|
||||
|
||||
## Example Using Local Development Instance of Talk
|
||||
If you have Talk running in a local development environment you can follow these steps to first view all of the events emitted by Talk, and then capture the `PostComment.success` event and send it to a third party analytics provider.
|
||||
|
||||
|
||||
First, with Talk running, ensure that you can access the `/dev/assets` endpoint and create a random article (i.e.: `localhost:3000/dev/assets`).
|
||||
|
||||
|
||||
Then in Talk, uncomment the events code in `/views/dev/article.njk`
|
||||
(https://github.com/coralproject/talk/blob/master/views/dev/article.njk#L32). This will enable events to be sent via the Talk embed that is on the sample article page. This will start a stream of events to the browser console, so that you can see which events are available.
|
||||
|
||||
```
|
||||
events: function(events) {
|
||||
@@ -16,7 +26,10 @@ First, we want to uncomment the tracking code in `article.ejs` (https://github.c
|
||||
},
|
||||
```
|
||||
|
||||
Now, we want to add our code that sends the events to our analytics system. In this case, we're sending the `PostComment.success` event. The particular way you send this will depend on what tool you're using. Refer to your tool's API and docs to determine this.
|
||||
Save your changes to `article.njk`, restart Talk, and try refreshing your article in a browser with development console open. You should see the console log the list of Talk’s events.
|
||||
|
||||
|
||||
Next, we want to add our code that sends the events to our analytics system. In this example, we're sending the `PostComment.success` event. The particular way you send this will depend on what tool you're using. Refer to your tool's API and docs to determine this.
|
||||
|
||||
```
|
||||
events: function(events) {
|
||||
@@ -30,4 +43,24 @@ Now, we want to add our code that sends the events to our analytics system. In t
|
||||
```
|
||||
You can continue this process for any specific events you'd like to track. You can also remove the `console.log` to stop events being emitted to the browser and instead only send the events to your analytics tool.
|
||||
|
||||
PR for Reference: https://github.com/coralproject/talk/pull/785
|
||||
## Metrics List
|
||||
|
||||
Note: This is not an exhaustive list, but should get you started.
|
||||
|
||||
```
|
||||
mutation.CreateRespectAction.success
|
||||
mutation.CreateLikeAction.success
|
||||
mutation.PostComment.success
|
||||
mutation.PostFlag.success
|
||||
mutation.EditComment.success
|
||||
|
||||
action.CORAL_STREAM_SET_ACTIVE_TAB
|
||||
action.SHOW_SIGNIN_DIALOG
|
||||
action.FOCUS_SIGNIN_DIALOG
|
||||
action.TALK_FRAMEWORK_HANDLE_SUCCESSFUL_LOGIN
|
||||
action.HIDE_SIGNIN_DIALOG
|
||||
action.TALK_VIEWING_OPTIONS_OPEN_MENU
|
||||
action.TALK_VIEWING_OPTIONS_CLOSE_MENU
|
||||
action.CORAL_STREAM_SET_SORT
|
||||
action.SET_ACTIVE_REPLY_BOX
|
||||
```
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: talk-plugin-notifications-category-moderation-actions
|
||||
permalink: /plugin/talk-plugin-notifications-category-moderation-actions/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-notifications-moderation-actions
|
||||
default: true
|
||||
depends:
|
||||
- name: talk-plugin-notifications
|
||||
provides:
|
||||
- Server
|
||||
- Client
|
||||
---
|
||||
|
||||
When a comment that is initially withheld from publication and is then approved or rejected, the user will receive a notification email.
|
||||
|
||||
Configuration:
|
||||
`TALK_MODERATION_NOTIFICATION_TYPES`. This plugin requires values to be set. Available options: `APPROVED`, `REJECTED` as a single string (comma separated).
|
||||
@@ -111,7 +111,7 @@ If you deploy using Docker, you can extend from the `*-onbuild` image, an
|
||||
example `Dockerfile` for your project could be:
|
||||
|
||||
```Dockerfile
|
||||
FROM coralproject/talk:4.5-onbuild
|
||||
FROM coralproject/talk:4-onbuild
|
||||
```
|
||||
|
||||
Establish a private repository for your instance that includes the following:
|
||||
|
||||
+3
-4
@@ -28,9 +28,8 @@
|
||||
|
||||
|
||||
<div class="notice">
|
||||
<p>Don't see what you're looking for? We're upgrading the docs, check out our <a href="https://github.com/coralproject/talk/tree/ef49d9a3d2acc4d2fc03b00e0c872dfbc57f005a/docs/_docs" target="_blank" class="coral-link-invert">old docs</a>.</p>
|
||||
<p><a href="https://coralproject.net/" class="coral-link-invert" target="_blank">The Coral Project</a> from Mozilla</p>
|
||||
<a href="https://www.mozilla.org/" target="_blank" title="Mozilla"><figure class="mozilla" alt="Mozilla">{{ image_tag('images/moz-logo-bw-rgb.svg') }}</figure></a>
|
||||
<p>Made by <a href="https://coralproject.net/" class="coral-link-invert" target="_blank">The Coral Project</a> by <a href="https://www.voxmedia.com/" class="coral-link-invert" target="_blank" title="Vox Media">Vox Media</a>.</p>
|
||||
<a href="https://www.voxmedia.com/" target="_blank" title="Vox Media"><figure class="voxmedia" alt="Vox Media">{{ image_tag('images/coralproject.png') }}</figure></a>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="sidebar__backdrop"></div>
|
||||
<div class="sidebar__backdrop"></div>
|
||||
|
||||
+25
-2
@@ -344,9 +344,13 @@ a.brand {
|
||||
padding: $content_gutter $content_gutter 60px;
|
||||
}
|
||||
|
||||
.mozilla {
|
||||
width: 50%;
|
||||
.voxmedia {
|
||||
// width: 50%;
|
||||
margin: 0 0 $sidebar_gutter;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@@ -461,3 +465,22 @@ a.brand {
|
||||
background: rgb(0, 102, 176);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.callout {
|
||||
background-color: $coral_button_background;
|
||||
color: white;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
@extend .coral-link;
|
||||
color: #FFF;
|
||||
border-color: #FFF;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
border-color: darken(#FFF, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 360 102.8" style="enable-background:new 0 0 360 102.8;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<rect y="-0.2" width="360" height="102.9"/>
|
||||
<path class="st0" d="M343.8,73.7c-0.7,0.2-1.3,0.3-1.9,0.3c-2.1,0-3.1-0.9-3.1-3.5V51c0-10.3-8.2-15.3-17.9-15.3
|
||||
c-7.4,0-11.4,0.9-19.3,4.1L300,50.2l10.3,1.1l1.5-5.1c2.1-1.1,4.2-1.3,6.9-1.3c7.3,0,7.4,5.5,7.4,10.1v1.5
|
||||
c-2.3-0.3-4.9-0.4-7.4-0.4c-10.3,0-21,2.6-21,13.7c0,9.4,7.4,12.9,13.9,12.9c7.3,0,11.9-4.4,14.5-9c0.6,5.5,3.9,9,9.9,9
|
||||
c2.8,0,5.7-0.8,8.1-2.1L343.8,73.7z M316.2,73.4c-3.9,0-5.3-2.3-5.3-5.2c0-4.9,4-6.2,8.6-6.2c2.1,0,4.4,0.3,6.5,0.6
|
||||
C325.7,70.1,320.8,73.4,316.2,73.4z M298.9,12.9l-19.5,68.7h-12.7l19.5-68.7H298.9z M272.7,12.9l-19.5,68.7h-12.7L260,12.9H272.7z
|
||||
M218.7,36.6h13.5v16.2h-13.5V36.6z M218.7,65.5h13.5v16.2h-13.5V65.5z M199,64.9l10.2,1l-2.8,15.8h-39.3l-1.3-6.8l24.8-28.5h-14.1
|
||||
l-2,7l-9.3-1l1.6-15.8h39.5l1,6.8l-25,28.5h14.6L199,64.9z M134.5,35.7c-16.2,0-24.2,10.9-24.2,24.1c0,14.4,9.6,22.9,23.5,22.9
|
||||
c14.4,0,24.8-9.1,24.8-23.5C158.6,46.6,150.7,35.7,134.5,35.7z M134.2,72.7c-7,0-10.6-6-10.6-13.8c0-8.5,4.1-13.4,10.7-13.4
|
||||
c6.1,0,11,4.1,11,13.2C145.3,67.4,140.9,72.7,134.2,72.7z M98.7,71.8h6v9.8H85.9V56.3c0-7.8-2.6-10.8-7.7-10.8
|
||||
c-6.2,0-8.7,4.4-8.7,10.7v15.7h6v9.8H56.6V56.3c0-7.8-2.6-10.8-7.7-10.8c-6.2,0-8.7,4.4-8.7,10.7v15.7h8.6v9.8H21.4v-9.8h6V46.4h-6
|
||||
v-9.8h18.9v6.8c2.7-4.8,7.4-7.7,13.7-7.7c6.5,0,12.5,3.1,14.7,9.7c2.5-6,7.6-9.7,14.7-9.7c8.1,0,15.5,4.9,15.5,15.6V71.8z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -64,13 +64,13 @@ const wrapCheck = (
|
||||
|
||||
/**
|
||||
* checkPermissions checks that the current user has all the required
|
||||
* permissions.
|
||||
* permissions. It will return true if that's the case.
|
||||
*
|
||||
* @param {Object} ctx graph context
|
||||
* @param {Array<String>} permissions permissions that the user must have
|
||||
*/
|
||||
const checkPermissions = (ctx, permissions) =>
|
||||
!ctx.user || !ctx.user.can(...permissions);
|
||||
ctx.user && ctx.user.can(...permissions);
|
||||
|
||||
/**
|
||||
* wrapCheckPermissions will wrap a specific field with a permission check.
|
||||
@@ -89,7 +89,7 @@ const wrapCheckPermissions = (
|
||||
wrapCheck(
|
||||
typeResolver,
|
||||
field,
|
||||
(obj, args, ctx) => !checkPermissions(ctx, permissions),
|
||||
(obj, args, ctx) => checkPermissions(ctx, permissions),
|
||||
skipFieldResolver
|
||||
);
|
||||
|
||||
|
||||
@@ -879,6 +879,9 @@ type Settings {
|
||||
# customCssUrl is the URL of the custom CSS used to display on the frontend.
|
||||
customCssUrl: String
|
||||
|
||||
# customAdminCssUrl is the URL of the custom CSS used to display on the admin panel.
|
||||
customAdminCssUrl: String
|
||||
|
||||
# closedTimeout is the amount of seconds from the created_at timestamp that a
|
||||
# given asset will be considered closed.
|
||||
closedTimeout: Int
|
||||
@@ -1353,6 +1356,9 @@ input UpdateSettingsInput {
|
||||
# customCssUrl is the URL of the custom CSS used to display on the frontend.
|
||||
customCssUrl: String
|
||||
|
||||
# customAdminCssUrl is the URL of the custom CSS used to display on the admin panel.
|
||||
customAdminCssUrl: String
|
||||
|
||||
# closedTimeout is the amount of seconds from the created_at timestamp that a
|
||||
# given asset will be considered closed.
|
||||
closedTimeout: Int
|
||||
|
||||
+6
-1
@@ -111,7 +111,12 @@ const processJob = transport => async ({ id, data }, done) => {
|
||||
const { message } = data;
|
||||
|
||||
// Get the email address from the job data.
|
||||
message.to = await getEmailAddress(data);
|
||||
try {
|
||||
message.to = await getEmailAddress(data);
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Failed to get user email address to send mail');
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const log = logger.child({ jobID: id });
|
||||
log.info('Starting to send mail');
|
||||
|
||||
+1
-3
@@ -312,7 +312,6 @@ ar:
|
||||
button: أرسل
|
||||
error: 'يمكن أن تحتوي أسماء المستخدمين على أحرف, أرقام و _ فقط'
|
||||
label: 'اسم مستخدم جديد'
|
||||
msg: 'تم تعليق حسابك حاليا نظرا لأن اسم المستخدم قد اعتبر غير لائق. لاستعادة حسابك، يرجى إدخال اسم مستخدم جديد. يرجى الاتصال بنا إذا كان لديك أي أسئلة.'
|
||||
my_comments: تعليقاتي
|
||||
my_profile: ملفي
|
||||
new_count: 'شاهد {0} أكثر {1}'
|
||||
@@ -359,7 +358,7 @@ ar:
|
||||
sign_in_button: 'تسجيل الدخول'
|
||||
sign_in_message: 'تسجيل الدخول للتفاعل مع مجتمعك.'
|
||||
password: كلمه المرور
|
||||
request_passowrd: 'اطلب واحدة جديده.'
|
||||
request_password: 'اطلب واحدة جديده.'
|
||||
team_sign_in: 'تسجيل الدخول لفريق العمل'
|
||||
marketing: 'هذا يشبه الإعلان / التسويق'
|
||||
moderate_all_streams: 'Moderate comments on All Stories'
|
||||
@@ -559,4 +558,3 @@ ar:
|
||||
view_conversation: 'عرض المحادثة'
|
||||
your_account_has_been_banned: 'تم حظر حسابك.'
|
||||
your_account_has_been_suspended: 'تم تعليق حسابك مؤقتا.'
|
||||
your_username_has_been_rejected: 'تم تعليق حسابك لعدم صلاحية اسم المستخدم الخاصة بك. لاستعادة حسابك رجاء أدخل اسم مستخدم جديدة.'
|
||||
|
||||
@@ -266,7 +266,6 @@ da:
|
||||
button: Indsend
|
||||
error: 'Brugernavne kan kun indeholde bogstaver og _'
|
||||
label: 'Nyt brugernavn'
|
||||
msg: 'Din konto er midlertidigt suspenderet, fordi dit brugernavn er blevet anset for upassende. For at gendanne din konto skal du indtaste et nyt brugernavn. Kontakt os venligst, hvis du har spørgsmål.'
|
||||
my_comments: 'Mine kommentarer'
|
||||
my_profile: 'Min profil'
|
||||
new_count: 'Se {0} mere {1}'
|
||||
@@ -473,4 +472,3 @@ da:
|
||||
view_conversation: 'Vis samtale'
|
||||
your_account_has_been_banned: 'Din konto er blevet banned.'
|
||||
your_account_has_been_suspended: 'Din konto er midlertidigt suspenderet'
|
||||
your_username_has_been_rejected: 'Din konto er blevet suspenderet fordi dit brugernavn er blevet anset for upassende. For at gendanne din konto, venligst indtast et nyt brugernavn.'
|
||||
|
||||
+77
-4
@@ -3,6 +3,13 @@ de:
|
||||
sort_comments: 'Kommentare sortieren'
|
||||
view_options: Ansichtsoptionen
|
||||
already_flagged_username: 'Sie haben diesen Nutzernamen schon markiert.'
|
||||
alwayspremoddialog:
|
||||
are_you_sure: 'Sind sie sicher, dass sie immer vormoderieren wollen für {0}?'
|
||||
always_premod_user: 'Nutzer immer vormoderieren?'
|
||||
cancel: Abbrechen
|
||||
note: 'Hinweis: {0}'
|
||||
note_always_premod_user: 'Kommentare dieses Nutzers werden niemals automatisch freigegeben sondern erscheinen immer in der Liste für Vormoderation.'
|
||||
yes_always_premod_user: 'Nutzer vormoderieren'
|
||||
bandialog:
|
||||
are_you_sure: 'Sind Sie sich sicher, dass Sie {0} sperren möchten?'
|
||||
ban_user: 'Nutzer sperren?'
|
||||
@@ -28,6 +35,9 @@ de:
|
||||
flagged: markiert
|
||||
undo_reject: 'Rückgängig machen'
|
||||
view_context: 'Kontext ansehen'
|
||||
edit_history: 'Verlauf anpassen'
|
||||
show_edit_history: 'Zeige Verlaufshistorie'
|
||||
hide_edit_history: 'Verstecke Verlaufshistorie'
|
||||
comment_box:
|
||||
cancel: Abbrechen
|
||||
characters_remaining: 'verbleibende Zeichen'
|
||||
@@ -48,9 +58,10 @@ de:
|
||||
comment_post_notif_premod: 'Vielen Dank für Ihren Kommentar. Unsere Moderatoren werden ihn in Kürze bearbeiten.'
|
||||
comment_singular: Kommentar
|
||||
common:
|
||||
copied: 'Kopiert'
|
||||
notsupported: 'Nicht unterstützt'
|
||||
contains_link: 'Enthält einen Link'
|
||||
copy: Kopieren
|
||||
copied: 'Kopiert'
|
||||
notsupported: 'Nicht unterstützt'
|
||||
error: 'Ein Problem ist aufgetreten.'
|
||||
reaction: 'Reaktion'
|
||||
reactions: 'Reaktionen'
|
||||
@@ -62,6 +73,8 @@ de:
|
||||
active: Aktiv
|
||||
admin: Administrator
|
||||
ads_marketing: 'Dies scheint Werbung zu sein'
|
||||
all: 'Alle'
|
||||
always_premod: 'Vormoderiert'
|
||||
are_you_sure: 'Sind Sie sich sicher, dass Sie {0} sperren möchten?'
|
||||
ban_user: 'Nutzer sperren?'
|
||||
banned: Gesperrt
|
||||
@@ -69,6 +82,8 @@ de:
|
||||
cancel: Abbrechen
|
||||
commenter: Kommentator
|
||||
dont_like_username: 'Unsympathischer Nutzername'
|
||||
filter_users: 'Nutzer filtern'
|
||||
filter_role: Rolle
|
||||
flaggedaccounts: 'Gemeldete Nutzernamen'
|
||||
flags: Markierungen
|
||||
impersonating: 'Identität unklar'
|
||||
@@ -85,6 +100,7 @@ de:
|
||||
spam_ads: Spam/Werbung
|
||||
staff: Mitarbeiter
|
||||
status: Status
|
||||
suspended: 'Vorübergehend gesperrt'
|
||||
username_and_email: 'Nutzername und E-Mail'
|
||||
yes_ban_user: 'Ja, Nutzer sperren'
|
||||
configure:
|
||||
@@ -111,6 +127,8 @@ de:
|
||||
copy_and_paste: 'Kopieren Sie diesen Code in Ihr CMS, um die Kommentarfunktion an entsprechender Stelle anzuzeigen.'
|
||||
custom_css_url: 'Benutzerdefinierte CSS-URL'
|
||||
custom_css_url_desc: 'URL eines CSS-Stylesheets zum Überschreiben des Standard-Designs'
|
||||
custom_admin_css_url: 'Benutzerdefinierte CSS URL für Admin Panel'
|
||||
custom_admin_css_url_desc: 'URL eines CSS-Stylesheets zum Überschreiben des Standart-Designs für das Admin Panel.'
|
||||
days: Tage
|
||||
description: 'Ändern Sie die Einstellungen für den Kommentarbereich dieses Artikels.'
|
||||
disable_commenting_desc: 'Verfassen Sie eine Nachricht, die angezeigt wird, solange das Kommentieren deaktiviert ist.'
|
||||
@@ -147,6 +165,9 @@ de:
|
||||
organization_info_copy_2: 'Wir empfehlen, einee generische E-Mail-Adresse (z.B. community@yournewsroom.com) für diesen Zweck einzurichten. Die kann über die Zeit gleich bleiben, und gibt nach außen keine Namen preis, die von Nutzern im Fall von Konflikten für persönliche Angriffe missbraucht werden könnten.'
|
||||
organization_information: 'Über die Organisation'
|
||||
organization_name: 'Name der Organisation'
|
||||
suspect_or_forbidden_words_placeholder: 'Wort oder Phrase'
|
||||
product_guide_link: 'Produkt Guide'
|
||||
report_bug_or_feedback: 'Feedback'
|
||||
require_email_verification: 'E-Mail-Bestätigung erforderlich'
|
||||
require_email_verification_text: 'Neue Nutzer müssen ihre E-Mail-Adresse bestätigen.'
|
||||
save: Speichern
|
||||
@@ -165,6 +186,7 @@ de:
|
||||
suspect_word_title: 'Liste verdächtiger Wörter'
|
||||
tech_settings: 'Technische Einstellungen'
|
||||
title: 'Kommentarbereich konfigurieren'
|
||||
view_last_version: 'Letzte Version'
|
||||
weeks: Wochen
|
||||
wordlist: 'Gesperrte Wörter'
|
||||
confirm_email:
|
||||
@@ -222,6 +244,7 @@ de:
|
||||
copied: 'Kopiert'
|
||||
error:
|
||||
ALREADY_EXISTS: 'Ressource existiert bereits'
|
||||
AUTHENTICATION: 'Bei der Anmeldung ist ein Fehler aufgetreten.'
|
||||
CANNOT_IGNORE_STAFF: 'Mitarbeiter können nicht ignoriert werden.'
|
||||
COMMENT_PARENT_NOT_VISIBLE: 'Der Kommentar, auf den Sie antworten möchten, wurde entfernt oder existiert nicht.'
|
||||
COMMENT_TOO_SHORT: 'Kommentare sollten mehr als ein Zeichen enthalten, bitte überprüfen Sie Ihren Kommentar und probieren Sie es erneut.'
|
||||
@@ -250,6 +273,7 @@ de:
|
||||
organization_contact_email: 'E-Mail-Adresse der Organisation ist ungültig.'
|
||||
organization_name: 'Namen von Organisationen dürfen nur Buchstaben und Zahlen enthalten.'
|
||||
password: 'Passwort muss mindestens 8 Zeichen enthalten'
|
||||
PAGE_NOT_AVAILABLE_ROLE: 'Der Zugriff auf diese Seite ist eingeschränkt. Bitte wenden Sie sich an den Administrator.'
|
||||
PASSWORD_INCORRECT: 'Ihr bestehendes Passwort wurde falsch eingegeben'
|
||||
PASSWORD_LENGTH: 'Passwort ist zu kurz'
|
||||
PASSWORD_REQUIRED: 'Passwort ist erforderlich'
|
||||
@@ -265,6 +289,13 @@ de:
|
||||
USERNAME_REQUIRED: 'Nutzername muss angegeben werden'
|
||||
flag_comment: 'Kommentar melden'
|
||||
flag_reason: 'Grund der Meldung (optional)'
|
||||
flag_reasons:
|
||||
username:
|
||||
impersonating: 'Gibt sich für jemand anderen aus'
|
||||
nolike: 'Ich mag diesen Nutzernamen nicht'
|
||||
offensive: 'Dieser Nutzername ist unangemessen'
|
||||
other: Sonstiges
|
||||
spam: 'Dies scheint Werbung zu sein'
|
||||
flag_username: 'Nutzername melden'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} hat Nutzername {1} freigegeben'
|
||||
@@ -281,6 +312,7 @@ de:
|
||||
comment_spam: Spam
|
||||
links: Link
|
||||
suspect_word: 'Verdächtiges Wort'
|
||||
trust: Karma
|
||||
user:
|
||||
username_impersonating: 'Identität unklar'
|
||||
username_nolike: Unerwünscht
|
||||
@@ -300,11 +332,11 @@ de:
|
||||
comments: Kommentare
|
||||
configure_stream: Konfigurieren
|
||||
content_not_available: 'Dieser Inhalt ist nicht verfügbar'
|
||||
edit: Ändern
|
||||
edit_name:
|
||||
button: Senden
|
||||
error: 'Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten'
|
||||
label: 'Neuer Nutzername'
|
||||
msg: 'Ihr Konto ist vorübergehend gesperrt, da Ihr Nutzername als unangemessen eingestuft wurde. Um Ihr Konto wieder herzustellen, geben Sie bitte einen neuen Nutzernamen ein. Bei Fragen, kontaktieren Sie uns bitte.'
|
||||
my_comments: 'Meine Kommentare'
|
||||
my_profile: 'Mein Profil'
|
||||
new_count: '{0} {1} mehr anzeigen'
|
||||
@@ -343,16 +375,30 @@ de:
|
||||
title: 'Zugelassene Domains'
|
||||
like: 'Gefällt mir'
|
||||
loading_results: 'Lädt Ergebnisse'
|
||||
login:
|
||||
email_address: 'E-Mail-Adresse'
|
||||
forgot_password: 'Passwort vergessen?'
|
||||
go_back: 'Zurück'
|
||||
sign_in: 'Anmelden'
|
||||
sign_in_button: 'Anmelden'
|
||||
sign_in_message: 'Anmelden um mit der Community zu interagieren.'
|
||||
password: Passwort
|
||||
reset_password_send_button: 'Passwort erhalten'
|
||||
request_password: 'Neues anfordern.'
|
||||
team_sign_in: 'Team Anmeldung'
|
||||
marketing: 'Dies scheint Werbung zu sein'
|
||||
moderate_all_streams: 'Moderate comments on All Stories'
|
||||
moderate_this_stream: 'Diesen Kommentarbereich moderieren'
|
||||
modqueue:
|
||||
account: Konto-Markierungen
|
||||
actions: Aktionen
|
||||
all: Alle
|
||||
all_streams: 'Alle Kommentarbereiche'
|
||||
always_premod_user_actions: 'Nutzer immer vormoderieren'
|
||||
approve: Freigeben
|
||||
approved: Freigegeben
|
||||
ban_user: Sperren
|
||||
ban_user_actions: 'Nutzer sperren'
|
||||
billion: Mrd
|
||||
close: Schließen
|
||||
empty_queue: 'Keine weiteren Kommentare zu moderieren! Arbeite nicht zu viel, gönn’ dir eine Pause!'
|
||||
@@ -387,6 +433,7 @@ de:
|
||||
show_shortcuts: 'Tastaturkürzel anzeigen'
|
||||
singleview: Zen-Modus
|
||||
sort: Sortieren
|
||||
suspend: 'Nutzer vorübergehend sperren'
|
||||
system_withheld: 'System Withheld'
|
||||
thismenu: 'Dieses Menü öffnen'
|
||||
thousand: T
|
||||
@@ -424,6 +471,13 @@ de:
|
||||
username: Nutzername
|
||||
write_message: 'Nachricht schreiben'
|
||||
yes_suspend: 'Ja, vorübergehend sperren'
|
||||
reject_username_dialog:
|
||||
cancel: Abbrechen
|
||||
description: 'Sagen Sie uns warun der Name nicht Ok ist?'
|
||||
message: 'Grund für die Meldung (optional)'
|
||||
reason: Begründung
|
||||
reject_username: 'Nutzernamen ablehnen'
|
||||
title: 'Nutzernamen ablehnen'
|
||||
reply: Antworten
|
||||
report: Melden
|
||||
report_notif: 'Vielen Dank für Ihre Meldung. Unsere Moderatoren wurden informiert und werden sich in Kürze darum kümmern.'
|
||||
@@ -452,11 +506,14 @@ de:
|
||||
closed: Geschlossen
|
||||
empty_result: 'Ihre Suche war ohne Ergebnisse. Versuchen Sie, Ihre Anfrage weiter zu fassen.'
|
||||
filter_streams: 'Kommentarbereiche filtern'
|
||||
most_recent_stories: 'Neueste Artikel'
|
||||
newest: Neueste
|
||||
no_results: 'Keine Ergebnisse'
|
||||
oldest: Älteste
|
||||
open: Offen
|
||||
pubdate: Veröffentlichungsdatum
|
||||
search: Suchen
|
||||
search_results: Suchergebnisse
|
||||
sort_by: 'Sortieren nach'
|
||||
status: Status
|
||||
stream_status: Status
|
||||
@@ -484,24 +541,41 @@ de:
|
||||
username_flags: 'Markierungen für diesen Nutzernamen'
|
||||
user_detail:
|
||||
all: Alle
|
||||
always_premod: 'Nutzer immer vormoderieren'
|
||||
always_premoded: 'Vormoderiert'
|
||||
ban: 'Nutzer sperren'
|
||||
banned: Gesperrt
|
||||
email: E-Mail
|
||||
id: ID
|
||||
karma: Karma
|
||||
karma_docs_link: 'https://docs.coralproject.net/talk/trust/#user-karma-score'
|
||||
learn_more: 'Mehr erfahren'
|
||||
member_since: 'Mitglied seit'
|
||||
reject_rate: Ablehn-Rate
|
||||
reject_username: 'Nutzernamen ablehnen'
|
||||
rejected: Abgelehnte
|
||||
remove_always_premod: 'Vormoderation entfernen'
|
||||
remove_ban: 'Sperre aufheben'
|
||||
remove_suspension: 'Vorübergehende Sperrung aufheben'
|
||||
suspend: 'Nutzer vorübergehend sperren'
|
||||
suspended: 'Vorübergehend gesperrt'
|
||||
total_comments: 'Anzahl Kommentare'
|
||||
unreliable: Unzuverlässig
|
||||
user_history: Konto-Verlauf
|
||||
user_karma_score: 'Nutzer Karma Wert'
|
||||
username: Username
|
||||
username_needs_approval: 'Nutzername muss akzetiert werden'
|
||||
username_rejected: 'Nutzername abgelehnt'
|
||||
user_history:
|
||||
action: Aktion
|
||||
always_premod_removed: 'Vormoderation entfernen'
|
||||
ban_removed: 'Sperrung aufgehoben'
|
||||
date: Datum
|
||||
suspended: 'Vorübergehend gesperrt, {0}'
|
||||
suspension_removed: 'Vorübergehende Sperrung aufgehoben'
|
||||
system: System
|
||||
taken_by: Durch
|
||||
user_always_premoded: 'Nutzer vormoderiert'
|
||||
user_banned: 'Nutzer gesperrt'
|
||||
username_status: 'Nutzername {0}'
|
||||
user_impersonating: 'Gibt sich für jemand anderen aus'
|
||||
@@ -518,4 +592,3 @@ de:
|
||||
view_conversation: 'Diskussion ansehen'
|
||||
your_account_has_been_banned: 'Ihr Zugang wurde gesperrt.'
|
||||
your_account_has_been_suspended: 'Ihr Zugang wurde vorübergehend gesperrt.'
|
||||
your_username_has_been_rejected: 'Ihr Zugang wurde vorübergehend gesperrt, da Ihr Nutzername als unangemessen eingestuft wurde. Um Ihren Zugang wieder herzustellen, geben Sie bitte einen neuen Nutzernamen ein.'
|
||||
|
||||
+9
-4
@@ -35,6 +35,9 @@ en:
|
||||
flagged: flagged
|
||||
undo_reject: Undo
|
||||
view_context: 'View context'
|
||||
edit_history: 'Edit history'
|
||||
show_edit_history: 'Show edit history'
|
||||
hide_edit_history: 'Hide edit history'
|
||||
comment_box:
|
||||
cancel: Cancel
|
||||
characters_remaining: 'characters remaining'
|
||||
@@ -124,6 +127,8 @@ en:
|
||||
copy_and_paste: 'Copy and paste code below into your CMS to embed your comment box in your articles'
|
||||
custom_css_url: 'Custom CSS URL'
|
||||
custom_css_url_desc: 'URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external.'
|
||||
custom_admin_css_url: 'Custom Admin Panel CSS URL'
|
||||
custom_admin_css_url_desc: 'URL of a CSS stylesheet that will override default admin panel styles. Can be internal or external.'
|
||||
days: Days
|
||||
description: 'Change the comment settings on this story.'
|
||||
disable_commenting_desc: 'Write a message that will be displayed while commenting is deactivated.'
|
||||
@@ -219,7 +224,7 @@ en:
|
||||
subject: 'Your account has been banned'
|
||||
confirm:
|
||||
confirm_email: 'Confirm Email'
|
||||
has_been_requested: 'A email confirmation has been requested for the following account:'
|
||||
has_been_requested: 'An email confirmation has been requested for the following account:'
|
||||
if_you_did_not: 'If you did not request this, you can safely ignore this email.'
|
||||
subject: 'Email Confirmation'
|
||||
to_confirm: 'To confirm the account, please visit the following link:'
|
||||
@@ -332,7 +337,7 @@ en:
|
||||
button: Submit
|
||||
error: 'Usernames can contain letters numbers and _ only'
|
||||
label: 'New Username'
|
||||
msg: 'Your account is currently suspended because your username has been deemed inappropriate. To restore your account please enter a new username. Please contact us if you have any questions.'
|
||||
rejected: 'Your choice of username has been rejected because it is not in line with our username policies. Please submit another name for moderator approval.'
|
||||
my_comments: 'My Comments'
|
||||
my_profile: 'My profile'
|
||||
new_count: 'View {0} more {1}'
|
||||
@@ -380,7 +385,7 @@ en:
|
||||
sign_in_message: 'Sign in to interact with your community.'
|
||||
password: Password
|
||||
reset_password_send_button: 'Retrieve Password'
|
||||
request_passowrd: 'Request a new one.'
|
||||
request_password: 'Request a new one.'
|
||||
team_sign_in: 'Team sign in'
|
||||
marketing: 'This looks like an ad/marketing'
|
||||
moderate_all_streams: 'Moderate comments on All Stories'
|
||||
@@ -588,4 +593,4 @@ en:
|
||||
view_conversation: 'View Conversation'
|
||||
your_account_has_been_banned: 'Your account has been banned.'
|
||||
your_account_has_been_suspended: 'Your account has been temporarily suspended.'
|
||||
your_username_has_been_rejected: 'Your account has been suspended because your username has been deemed inappropriate. To restore your account please enter a new username.'
|
||||
your_username_has_been_rejected_not_in_line: 'Your choice of username has been rejected because it is not in line with our username policies. Please submit another name for moderator approval.'
|
||||
|
||||
+3
-2
@@ -35,6 +35,9 @@ es:
|
||||
flagged: Reportado
|
||||
undo_reject: Deshacer
|
||||
view_context: 'Ver contexto'
|
||||
edit_history: 'Historial de ediciones'
|
||||
show_edit_history: 'Mostrar historial de ediciones'
|
||||
hide_edit_history: 'Ocultar historial de ediciones'
|
||||
comment_box:
|
||||
cancel: Cancelar
|
||||
characters_remaining: 'caracteres restantes'
|
||||
@@ -306,7 +309,6 @@ es:
|
||||
button: Enviar
|
||||
error: 'Nombres de usuarios pueden solamente incluir letras, números y _'
|
||||
label: 'Nuevo Nombre'
|
||||
msg: 'Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta.'
|
||||
my_comments: 'Mis Comentarios'
|
||||
my_profile: 'Mi perfil'
|
||||
new_count: 'Ver {0} más {1} '
|
||||
@@ -539,4 +541,3 @@ es:
|
||||
view_conversation: 'Ver Conversación'
|
||||
your_account_has_been_banned: 'Su cuenta ha sido prohibida.'
|
||||
your_account_has_been_suspended: 'Su cuenta ha sido suspendida.'
|
||||
your_username_has_been_rejected: 'Su cuenta ha sido suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario.'
|
||||
|
||||
@@ -269,7 +269,6 @@ fi_FI:
|
||||
button: Lähetä
|
||||
error: 'Käyttäjänimissä sallittuja merkkejä ovat ainoastaan kirjaimet, numerot, sekä alaviiva'
|
||||
label: 'Uusi käyttäjänimi'
|
||||
msg: 'Tilisi on suljettu väliaikaisesti, koska käyttäjänimi on todettu sopimattomaksi. Vaihda käyttäjänimi, jos haluat jatkaa tilin käyttöä. Ole meihin yhteydessä, jos sinulla on aiheesta kysyttävää.'
|
||||
my_comments: Kommenttini
|
||||
my_profile: Profiilini
|
||||
new_count: 'Näytä {0} lisää {1}'
|
||||
@@ -466,4 +465,3 @@ fi_FI:
|
||||
view_conversation: 'Näytä keskustelu'
|
||||
your_account_has_been_banned: 'Tilillesi on asetettu kirjoituskielto.'
|
||||
your_account_has_been_suspended: 'Tilisi on väliaikasesti suljettu.'
|
||||
your_username_has_been_rejected: 'Tilisi on suljettu, koska käyttäjänimesi on epäsopiva. Vaihda käyttäjänimeä jatkaaksesi tilin käyttöä.'
|
||||
|
||||
@@ -264,7 +264,6 @@ fr:
|
||||
button: Soumettre
|
||||
error: 'Les noms d''utilisateur ne peuvent contenir que des chiffres, des lettres et "_"'
|
||||
label: 'Nouveau nom d''utilisateur'
|
||||
msg: 'Votre compte est actuellement suspendu car votre nom d''utilisateur a été jugé inapproprié. Pour restaurer votre compte, entrez un nouveau nom d''utilisateur. Contactez-nous si vous avez des questions.'
|
||||
my_comments: 'Mes commentaires'
|
||||
my_profile: 'Mon profil'
|
||||
new_count: 'Voir {0} plus {1}'
|
||||
@@ -462,4 +461,3 @@ fr:
|
||||
view_conversation: 'Afficher la conversation'
|
||||
your_account_has_been_banned: 'Votre compte a été banni.'
|
||||
your_account_has_been_suspended: 'Votre compte a été temporairement suspendu.'
|
||||
your_username_has_been_rejected: 'Votre compte a été suspendu en raison de votre nom d’utilisateur jugé inapproprié. Veuillez saisir un nouveau nom d’utilisateur pour restaurer votre compte.'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
he:
|
||||
your_account_has_been_suspended: החשבון שלך הושעה באופן זמני.
|
||||
your_account_has_been_banned: החשבון שלך הורחק.
|
||||
your_username_has_been_rejected: החשבון שלך הושעה מפני ששם המשתמש שלך נחשב לא הולם. כדי לשחזר את החשבון שלך, הזן שם משתמש חדש.
|
||||
embed_comments_tab: תגובות
|
||||
bandialog:
|
||||
are_you_sure: "האם אתה בטוח שאתה רוצה לאסור {0}?"
|
||||
@@ -260,7 +259,6 @@ he:
|
||||
button: שלח
|
||||
error: "שמות משתמש יכולים להכיל מספרי אותיות ו- _ בלבד"
|
||||
label: "שם משתמש חדש"
|
||||
msg: "החשבון שלך מושעה כרגע מפני ששם המשתמש שלך נחשב לא הולם. כדי לשחזר את החשבון שלך, הזן שם משתמש חדש. אנא צור איתנו קשר אם יש לך שאלות."
|
||||
changed_name:
|
||||
msg: "שינוי שם המשתמש שלך נמצא בבדיקה על ידי צוות הניהול שלנו."
|
||||
my_comments: "התגובות שלי"
|
||||
|
||||
+1
-3
@@ -321,7 +321,6 @@ it:
|
||||
button: Invia
|
||||
error: 'L''username può contenere solo lettere numeri o _'
|
||||
label: 'Nuovo username'
|
||||
msg: 'Il tuo account è attualmente sospeso perchè il tuo username è considerato inappropriato. Per ripristinare il tuo account inserisci un nuovo username. Contattaci se hai qualche domanda.'
|
||||
my_comments: 'I miei commenti'
|
||||
my_profile: 'Il mio profilo'
|
||||
new_count: 'Visualizza {0} e {1}'
|
||||
@@ -369,7 +368,7 @@ it:
|
||||
sign_in_message: 'Accedi per interagire con la comunità.'
|
||||
password: Password
|
||||
reset_password_send_button: 'Reimposta Password'
|
||||
request_passowrd: 'Richiedi una nuova password.'
|
||||
request_password: 'Richiedi una nuova password.'
|
||||
team_sign_in: 'Accesso Team'
|
||||
marketing: 'Sembra si tratti di pubblicità/marketing'
|
||||
moderate_all_streams: 'Modera i commenti in Tutte le storie'
|
||||
@@ -571,4 +570,3 @@ it:
|
||||
view_conversation: 'Visualizza conversazione'
|
||||
your_account_has_been_banned: 'Il tuo account è stato bloccato.'
|
||||
your_account_has_been_suspended: 'Il tuo account è stato temporaneamente sospeso.'
|
||||
your_username_has_been_rejected: 'Il tuo account è stato sospeso perchè il tuo username è ritenuto inappropriato. Per ripristinare il tuo account inserisci un nuovo username.'
|
||||
|
||||
+1
-3
@@ -324,7 +324,6 @@ nl_NL:
|
||||
button: Versturen
|
||||
error: "Gebruikersnamen kunnen alleen cijfers, letters en _ bevatten."
|
||||
label: "Nieuwe gebruikersnaam"
|
||||
msg: "Je account is momenteel geschorst omdat je gebruikersnaam ongepast is bevonden. Om je account te herstellen, moet je je gebruikersnaam herstellen. Neem contact met ons op wanneer je vragen hebt."
|
||||
my_comments: "Mijn reacties"
|
||||
my_profile: "Mijn profiel"
|
||||
new_count: "Bekijk {0} meer {1}"
|
||||
@@ -372,7 +371,7 @@ nl_NL:
|
||||
sign_in_message: "Log in om te communiceren met je community"
|
||||
password: Wachtwoord
|
||||
reset_password_send_button: "Wachtwoord ophalen"
|
||||
request_passowrd: "Vraag een nieuwe aan."
|
||||
request_password: "Vraag een nieuwe aan."
|
||||
team_sign_in: "Team login"
|
||||
marketing: "Dit lijkt op een advertentie/marketing"
|
||||
moderate_all_streams: "Modereer reacties op alle artikelen"
|
||||
@@ -574,4 +573,3 @@ nl_NL:
|
||||
view_conversation: "Bekijk conversatie"
|
||||
your_account_has_been_banned: "Je account is verbannen."
|
||||
your_account_has_been_suspended: "Je account is tijdelijk geschorst."
|
||||
your_username_has_been_rejected: "Je account is geschorst omdat we je gebruikersnaam ongepast vinden. Verander je gebruikersnaam om hem weer te activeren."
|
||||
|
||||
+1
-3
@@ -285,7 +285,6 @@ pt_BR:
|
||||
button: Enviar
|
||||
error: 'Nomes de usuários podem conter números de letras e _ somente'
|
||||
label: 'Novo usuário'
|
||||
msg: 'Sua conta está suspensa porque seu nome de usuário foi considerado inapropriado. Para restaurar sua conta, insira um novo nome de usuário. Entre em contato conosco se você tiver alguma dúvida.'
|
||||
my_comments: 'Meus comentários'
|
||||
my_profile: 'Meu perfil'
|
||||
new_count: 'Ver {0} {1}'
|
||||
@@ -333,7 +332,7 @@ pt_BR:
|
||||
sign_in_message: 'Entre para interagir com a comunidade.'
|
||||
password: Senha
|
||||
reset_password_send_button: 'Recuperar Senha'
|
||||
request_passowrd: 'Recupere-a clicando aqui.'
|
||||
request_password: 'Recupere-a clicando aqui.'
|
||||
team_sign_in: 'Team sign in'
|
||||
marketing: 'Isso parece um anúncio/marketing'
|
||||
moderate_all_streams: 'Comentários moderados em todas as matérias'
|
||||
@@ -522,4 +521,3 @@ pt_BR:
|
||||
view_conversation: 'Ver conversa'
|
||||
your_account_has_been_banned: 'Sua conta foi banida.'
|
||||
your_account_has_been_suspended: 'Sua conta foi temporariamente suspensa.'
|
||||
your_username_has_been_rejected: 'Sua conta foi rejeitada porque seu nome de usuário foi considerado inapropriado. Para restaurar sua conta, insira um novo nome de usuário.'
|
||||
|
||||
+1
-3
@@ -323,7 +323,6 @@ sr:
|
||||
button: 'Podnesi'
|
||||
error: 'Došlo je do greške'
|
||||
label: 'Novo korisničko ime'
|
||||
msg: 'Izmjena vašeg korisničkog imena prolazi provjeru našeg tima moderatora'
|
||||
my_comments: 'Moji komentari'
|
||||
my_profile: 'Moj profil'
|
||||
new_count: 'Vidi još {0} / {1}'
|
||||
@@ -371,7 +370,7 @@ sr:
|
||||
sign_in_message: 'Prijavi se kako bi komunikacirao sa zajednicom'
|
||||
password: 'Šifra mora sadržati minimum 8 karaktera'
|
||||
reset_password_send_button: 'povrati šifru'
|
||||
request_passowrd: 'Zahtijevaj novu šifru'
|
||||
request_password: 'Zahtijevaj novu šifru'
|
||||
team_sign_in: 'Prijava tima'
|
||||
marketing: 'Ovo izgleda kao reklama/marketing'
|
||||
moderate_all_streams: 'Moderiraj komentare na svim pričama'
|
||||
@@ -573,4 +572,3 @@ sr:
|
||||
view_conversation: 'Vidi razgovor'
|
||||
your_account_has_been_banned: 'Vaš nalog je banovan'
|
||||
your_account_has_been_suspended: 'Vaš nalog je privremeno suspendovan'
|
||||
your_username_has_been_rejected: 'Vaš nalog je suspendovan jer je korisničko ime označeno kao neprikladno. Da povratite nalog, unesite novo korisničko ime'
|
||||
|
||||
@@ -232,7 +232,6 @@ zh_CN:
|
||||
button: 提交
|
||||
error: 用户名只能包含字母、数字跟下划线
|
||||
label: 新用户名
|
||||
msg: 由于您的用户名不当,您的帐号目前被暂停使用。如要恢复您的帐户,请输入一个新的用户名。如有任何疑问,请与我们联系。
|
||||
my_comments: 我的评论
|
||||
my_profile: 我的资料
|
||||
new_count: '查看 {0} 更多 {1}'
|
||||
@@ -411,4 +410,3 @@ zh_CN:
|
||||
view_conversation: 查看对话
|
||||
your_account_has_been_banned: 您的帐户已被禁用。
|
||||
your_account_has_been_suspended: 您的帐户已被暂时停用。
|
||||
your_username_has_been_rejected: 由于您使用了不恰当的用户名,您的帐户已被暂停使用。若需恢复帐户,请输入一个新的用户名
|
||||
|
||||
@@ -228,7 +228,6 @@ zh_TW:
|
||||
button: 提交
|
||||
error: 用戶名只能包含字母、數字和下劃線。
|
||||
label: 新用戶名
|
||||
msg: 由於您的用戶名不當,您的帳號目前已被暫停使用。如要恢復您的帳戶,請輸入一個新的用戶名。如有任何疑問,請與我們聯繫。
|
||||
my_comments: 我的評論
|
||||
my_profile: 我的概況
|
||||
new_count: '查看{0}更多{1}'
|
||||
@@ -407,4 +406,3 @@ zh_TW:
|
||||
view_conversation: 查看對話
|
||||
your_account_has_been_banned: 您的賬戶已被禁用。
|
||||
your_account_has_been_suspended: 您的賬戶已被暫停使用。
|
||||
your_username_has_been_rejected: 由於您使用了不恰當的用戶名,您的帳戶已被暫停使用。若需恢復帳戶,請輸入一個新的用戶名。
|
||||
|
||||
@@ -12,6 +12,8 @@ const {
|
||||
STATIC_ORIGIN,
|
||||
} = require('../url');
|
||||
|
||||
const { PORT } = require('../config');
|
||||
|
||||
const { RECAPTCHA_PUBLIC, WEBSOCKET_LIVE_URI } = require('../config');
|
||||
|
||||
// Grab TALK_CLIENT_* environment variables.
|
||||
@@ -42,6 +44,7 @@ const TEMPLATE_LOCALS = {
|
||||
MOUNT_PATH,
|
||||
STATIC_URL,
|
||||
TALK_CLIENT_ENV,
|
||||
PORT,
|
||||
data: TALK_CLIENT_ENV,
|
||||
};
|
||||
|
||||
@@ -96,12 +99,18 @@ const createResolveFactory = (() => {
|
||||
|
||||
module.exports = async (req, res, next) => {
|
||||
try {
|
||||
// Attach the custom css url and organization name.
|
||||
const { customCssUrl, organizationName } = await SettingsService.select(
|
||||
// Attach the custom css urls and organization name.
|
||||
const {
|
||||
customCssUrl,
|
||||
customAdminCssUrl,
|
||||
organizationName,
|
||||
} = await SettingsService.select(
|
||||
'customCssUrl',
|
||||
'customAdminCssUrl',
|
||||
'organizationName'
|
||||
);
|
||||
res.locals.customCssUrl = customCssUrl;
|
||||
res.locals.customAdminCssUrl = customAdminCssUrl;
|
||||
res.locals.organizationName = organizationName;
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
|
||||
@@ -28,6 +28,10 @@ const Setting = new Schema(
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
customAdminCssUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
infoBoxContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "4.8.3",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"version": "4.10.3",
|
||||
"description": "A better commenting experience from Vox Media.",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
+22
-6
@@ -1,26 +1,42 @@
|
||||
{
|
||||
"server": [
|
||||
"talk-plugin-featured-comments",
|
||||
"talk-plugin-local-auth",
|
||||
"talk-plugin-featured-comments",
|
||||
"talk-plugin-respect",
|
||||
"talk-plugin-rich-text",
|
||||
"talk-plugin-profile-data",
|
||||
"talk-plugin-respect"
|
||||
"talk-plugin-notifications",
|
||||
"talk-plugin-notifications-category-featured",
|
||||
"talk-plugin-notifications-category-moderation-actions",
|
||||
"talk-plugin-notifications-category-reply",
|
||||
"talk-plugin-notifications-category-staff",
|
||||
"talk-plugin-notifications-digest-hourly",
|
||||
"talk-plugin-notifications-digest-daily"
|
||||
],
|
||||
"client": [
|
||||
"talk-plugin-auth",
|
||||
"talk-plugin-local-auth",
|
||||
"talk-plugin-author-menu",
|
||||
"talk-plugin-featured-comments",
|
||||
"talk-plugin-flag-details",
|
||||
"talk-plugin-ignore-user",
|
||||
"talk-plugin-local-auth",
|
||||
"talk-plugin-member-since",
|
||||
"talk-plugin-moderation-actions",
|
||||
"talk-plugin-permalink",
|
||||
"talk-plugin-profile-data",
|
||||
"talk-plugin-respect",
|
||||
"talk-plugin-rich-text",
|
||||
"talk-plugin-viewing-options",
|
||||
"talk-plugin-sort-most-replied",
|
||||
"talk-plugin-sort-most-respected",
|
||||
"talk-plugin-sort-newest",
|
||||
"talk-plugin-sort-oldest",
|
||||
"talk-plugin-viewing-options"
|
||||
"talk-plugin-profile-data",
|
||||
"talk-plugin-notifications",
|
||||
"talk-plugin-notifications-category-featured",
|
||||
"talk-plugin-notifications-category-moderation-actions",
|
||||
"talk-plugin-notifications-category-reply",
|
||||
"talk-plugin-notifications-category-staff",
|
||||
"talk-plugin-notifications-digest-hourly",
|
||||
"talk-plugin-notifications-digest-daily",
|
||||
"talk-plugin-ignore-user"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ permalink: /plugin/talk-plugin-comment-content/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-comment-content
|
||||
default: true
|
||||
provides:
|
||||
- Client
|
||||
---
|
||||
@@ -12,4 +11,4 @@ plugin:
|
||||
Pluginizes the text of a comment to support custom treatment of this text. This
|
||||
plugin currently parses the given text to see if it contains a link, and makes
|
||||
them clickable using
|
||||
[react-linkify](https://www.npmjs.com/package/react-linkify).
|
||||
[react-linkify](https://www.npmjs.com/package/react-linkify).
|
||||
|
||||
@@ -11,25 +11,44 @@ plugin:
|
||||
- Client
|
||||
---
|
||||
|
||||
Enables sign-in via Facebook via the server side passport middleware.
|
||||
Enables sign-in via Facebook via the server side passport middleware. Requires creating and registering a login app with Facebook.
|
||||
|
||||
Configuration:
|
||||
**Configuration:**
|
||||
|
||||
- `TALK_FACEBOOK_APP_ID` (**required**) - The Facebook App ID for your Facebook
|
||||
Login enabled app. You can learn more about getting a Facebook App ID at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com) or by visiting
|
||||
the [Creating an App ID](https://developers.facebook.com/docs/apps/register)
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
Login enabled app. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
- `TALK_FACEBOOK_APP_SECRET` (**required**) - The Facebook App Secret for your
|
||||
Facebook Login enabled app. You can learn more about getting a Facebook App
|
||||
Secret at the [Facebook Developers Portal](https://developers.facebook.com)
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register)
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
Facebook Login enabled app. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
You can learn more about getting a Facebook App ID at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com) or by visiting
|
||||
their [Creating an App ID](https://developers.facebook.com/docs/apps/register)
|
||||
guide.
|
||||
|
||||
_NOTE: FabceBook auth requires your site to use `https` (SSL) not `http`. If your site is not `https` you can not use this plugin!_
|
||||
**Setting up your Facebook app:**
|
||||
* Go to [Facebook Developers Portal](https://developers.facebook.com) and click on Getting Started or My Apps
|
||||
* Create a new app > set the App Name and Email to create an app id
|
||||
* Confirm that you are not a robot, then configure the app as follows:
|
||||
* In Settings > Basic:
|
||||
* add app domains (your Talk domain)
|
||||
* add a link to your privacy policy
|
||||
* add a link to your terms of service
|
||||
* In Settings > Advanced:
|
||||
* disable "Require App Secret"
|
||||
* Add a "Product" (Under "Products" click + to add a Product):
|
||||
* Setup "Facebook Login"
|
||||
* choose `www`
|
||||
* enter your Talk domain url
|
||||
* click _Next_ several times to get through the add code steps (You do not need to modify any code, the plugin takes care of this part for you.)
|
||||
* Under Product Settings:
|
||||
* set Valid OAuth Redirect URIs to your callback url (Use your Talk domain with this endpoint: `/api/v1/auth/facebook/callback`)
|
||||
* Locate your App Id and App Secret, set these as config vars on your instance of Talk
|
||||
* Toggle the "Live" button on the top bar to make app live
|
||||
|
||||
|
||||
_NOTE: Facebook auth requires your site to use `https` (SSL) not `http`. If your site is not `https` you can not use this plugin!_
|
||||
|
||||
## GDPR Compliance
|
||||
|
||||
|
||||
@@ -14,6 +14,17 @@ Enables the ability for Moderators to feature and un-feature comments via the
|
||||
Stream and the Admin. Featured comments show in a first-place tab on the Stream
|
||||
if there are any featured comments on that story.
|
||||
|
||||
When paired with the [talk-plugin-moderator-actions](/talk/plugin/talk-plugin-moderator-actions)
|
||||
When paired with the [talk-plugin-moderation-actions](/talk/plugin/talk-plugin-moderation-actions)
|
||||
plugin, moderators will have the option of featuring comments from the comment
|
||||
stream.
|
||||
stream.
|
||||
|
||||
## Default Comments Tab
|
||||
|
||||
You can also change the default tab when rendering the comment stream to the
|
||||
featured tab so when a user reaches a comment stream, they will see the featured
|
||||
comments first by setting the [TALK_DEFAULT_STREAM_TAB](/talk/advanced-configuration/#talk-default-stream-tab)
|
||||
variable to `talk-plugin-featured-comments`.
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild](/talk/installation-from-docker/#onbuild)
|
||||
image you can specify it with `--build-arg TALK_DEFAULT_STREAM_TAB=talk-plugin-featured-comments`.
|
||||
|
||||
@@ -42,19 +42,10 @@
|
||||
text-align: left;
|
||||
letter-spacing: 0.1px;
|
||||
margin: 0;
|
||||
quotes: '\201c' '\201d';
|
||||
margin-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.quote:before {
|
||||
content: open-quote;
|
||||
}
|
||||
|
||||
.quote:after {
|
||||
content: close-quote;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -38,13 +38,15 @@
|
||||
color: #484747;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 0 16px 0 0;
|
||||
font-size: 1.02em;
|
||||
margin-left: 6px;
|
||||
letter-spacing: 0.2px;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 2px;
|
||||
line-height: 22px;
|
||||
box-sizing: border-box;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -36,17 +36,32 @@
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #787D80;
|
||||
background-color: #3498db;
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-size: 0.9em;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
background-color: #3498DB;
|
||||
border: 0;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
font-size: 1em;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel {
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
border: 0;
|
||||
color: #4f5c67;
|
||||
display: inline-block;
|
||||
font-size: 1em;
|
||||
height: 30px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
} from 'coral-framework/lib/validation';
|
||||
import { Form, Field } from 'react-final-form';
|
||||
|
||||
const AddEmailContent = ({ onSubmit }) => (
|
||||
const AddEmailContent = ({ onSubmit, onCancel }) => (
|
||||
<div>
|
||||
<h4 className={styles.title}>
|
||||
{t('talk-plugin-local-auth.add_email.content.title')}
|
||||
{t('talk-plugin-local-auth.add_email.title')}
|
||||
</h4>
|
||||
<p className={styles.description}>
|
||||
{t('talk-plugin-local-auth.add_email.content.description')}
|
||||
@@ -113,6 +113,14 @@ const AddEmailContent = ({ onSubmit }) => (
|
||||
<button className={styles.button} disabled={submitting}>
|
||||
{t('talk-plugin-local-auth.add_email.add_email_address')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className={styles.cancel}
|
||||
disabled={submitting}
|
||||
type="button"
|
||||
>
|
||||
{t('talk-plugin-local-auth.add_email.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
@@ -122,6 +130,7 @@ const AddEmailContent = ({ onSubmit }) => (
|
||||
|
||||
AddEmailContent.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddEmailContent;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.dialog {
|
||||
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: 400px;
|
||||
top: 10px;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
|
||||
import { logout } from 'plugin-api/beta/client/actions/auth';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { withAttachLocalAuth } from '../hocs';
|
||||
import { startAttach, finishAttach } from '../actions';
|
||||
@@ -17,6 +18,8 @@ import {
|
||||
EmailAddressAdded,
|
||||
} from '../components/AddEmailAddress';
|
||||
|
||||
import styles from './AddEmailAddressDialog.css';
|
||||
|
||||
class AddEmailAddressDialog extends React.Component {
|
||||
state = {
|
||||
step: 0,
|
||||
@@ -56,6 +59,10 @@ class AddEmailAddressDialog extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleOnCancel = async () => {
|
||||
this.props.logout();
|
||||
};
|
||||
|
||||
goToNextStep = () => {
|
||||
this.setState(({ step }) => ({
|
||||
step: step + 1,
|
||||
@@ -72,8 +79,17 @@ class AddEmailAddressDialog extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Dialog open={true} id="talk-plugin-local-auth-email-dialog">
|
||||
{step === 0 && <AddEmailForm onSubmit={this.handleSubmit} />}
|
||||
<Dialog
|
||||
open={true}
|
||||
id="talk-plugin-local-auth-email-dialog"
|
||||
className={styles.dialog}
|
||||
>
|
||||
{step === 0 && (
|
||||
<AddEmailForm
|
||||
onSubmit={this.handleSubmit}
|
||||
onCancel={this.handleOnCancel}
|
||||
/>
|
||||
)}
|
||||
{step === 1 &&
|
||||
!requireEmailConfirmation && (
|
||||
<EmailAddressAdded onDone={this.handleDone} />
|
||||
@@ -92,6 +108,7 @@ AddEmailAddressDialog.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
startAttach: PropTypes.func.isRequired,
|
||||
finishAttach: PropTypes.func.isRequired,
|
||||
logout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -100,7 +117,7 @@ const mapStateToProps = ({ talkPluginLocalAuth: state }) => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ notify, startAttach, finishAttach }, dispatch);
|
||||
bindActionCreators({ notify, startAttach, finishAttach, logout }, dispatch);
|
||||
|
||||
const withData = withFragments({
|
||||
root: gql`
|
||||
|
||||
@@ -32,4 +32,18 @@ class ErrIncorrectPassword extends TalkError {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ErrLocalProfile, ErrNoLocalProfile, ErrIncorrectPassword };
|
||||
class ErrDuplicateLocalProfile extends TalkError {
|
||||
constructor() {
|
||||
super('Duplicate local profile attachment', {
|
||||
translation_key: 'DUPLICATE_LOCAL_PROFILE',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ErrDuplicateLocalProfile,
|
||||
ErrLocalProfile,
|
||||
ErrNoLocalProfile,
|
||||
ErrIncorrectPassword,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ const {
|
||||
ErrNoLocalProfile,
|
||||
ErrLocalProfile,
|
||||
ErrIncorrectPassword,
|
||||
ErrDuplicateLocalProfile,
|
||||
} = require('./errors');
|
||||
const { get } = require('lodash');
|
||||
|
||||
@@ -115,6 +116,11 @@ async function attachUserLocalAuth(ctx, email, password) {
|
||||
// Validate the password.
|
||||
await Users.isValidPassword(password);
|
||||
|
||||
// See if this email address already has a local profile setup.
|
||||
if (await Users.findLocalUser(email)) {
|
||||
throw new ErrDuplicateLocalProfile();
|
||||
}
|
||||
|
||||
// Hash the new password.
|
||||
const hashedPassword = await Users.hashPassword(password);
|
||||
|
||||
@@ -160,7 +166,7 @@ async function attachUserLocalAuth(ctx, email, password) {
|
||||
await Users.sendEmailConfirmation(updatedUser, email, redirectUri);
|
||||
} catch (err) {
|
||||
if (err.code === 11000) {
|
||||
throw new ErrEmailTaken();
|
||||
throw new ErrDuplicateLocalProfile();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ ar:
|
||||
required_field: "هذه الخانة مطلوبه"
|
||||
done: "تم"
|
||||
content:
|
||||
title: "أضف عنوان بريد إلكتروني"
|
||||
description: "لمزيد من الأمان ، نطلب من المستخدمين إضافة عنوان بريد إلكتروني إلى حساباتهم. سيتم استخدام عنوان بريدك الإلكتروني في:"
|
||||
item_1: "تلقي التحديثات المتعلقة بأي تغييرات في حسابك (عنوان البريد الإلكتروني ، اسم المستخدم ، كلمة المرور ، إلخ.)"
|
||||
item_2: "السماح لك بتنزيل تعليقاتك."
|
||||
@@ -83,6 +82,7 @@ en:
|
||||
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.
|
||||
DUPLICATE_LOCAL_PROFILE: An account already exists with this email address. Please cancel and login with your email address and password.
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
change_password: "Change Password"
|
||||
@@ -125,7 +125,9 @@ en:
|
||||
cancel: "Cancel"
|
||||
change_email_msg: "Email Address Changed. This email address will now be used for signing in and email notifications."
|
||||
add_email:
|
||||
title: "Create New Account"
|
||||
add_email_address: "Add Email Address"
|
||||
cancel: "Cancel"
|
||||
enter_email_address: "Enter Email Address:"
|
||||
invalid_email_address: "Invalid Email address"
|
||||
confirm_email_address: "Confirm Email Address:"
|
||||
@@ -135,7 +137,6 @@ en:
|
||||
required_field: "This field is required"
|
||||
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:"
|
||||
item_1: "Receive updates regarding any changes to your account (email address, username, password, etc.)"
|
||||
item_2: "Allow you to download your comments."
|
||||
@@ -211,7 +212,6 @@ sr:
|
||||
required_field: "Ovo polje je obavezno"
|
||||
done: "Gotovo"
|
||||
content:
|
||||
title: "Dodaj e-mail adresu"
|
||||
description: "For your added security, we require users to add an email address to their accounts. Your email address will be used to:"
|
||||
item_1: "Receive updates regarding any changes to your account (email address, username, password, etc.)"
|
||||
item_2: "Omogućuje vam da preuzmete (download) svoje komentare."
|
||||
@@ -225,7 +225,7 @@ sr:
|
||||
subtitle: "Želite da promenite e-mail adresu?"
|
||||
description_2: "Možete urediti svoj nalog ovde:"
|
||||
path: "Moj profil > Podešavanja"
|
||||
alert: "E-mail dodat"
|
||||
alert: "E-mail dodat"
|
||||
pt_BR:
|
||||
email:
|
||||
email_change_original:
|
||||
@@ -287,7 +287,6 @@ pt_BR:
|
||||
required_field: "Esse campo é obrigatório"
|
||||
done: "Feito"
|
||||
content:
|
||||
title: "Adicione um endereço de email"
|
||||
description: "Para a sua segurança, exigimos que os usuários adicionem um endereço de email para suas contas. Seu email será usado para:"
|
||||
item_1: "Receba avisos de alterações na sua conta(endereço de email, usuário, senha, etc.)"
|
||||
item_2: "Permitir que você baixe seus comentários."
|
||||
@@ -344,7 +343,7 @@ de:
|
||||
change_username_attempt: "Der Nutzername kann zur Zeit nicht aktualisiert werden. Änderungen sind nur nach jeweils 14 Tagen möglich."
|
||||
change_email:
|
||||
confirm_email_change: "Änderung der E-Mail-Adresse bestätigen"
|
||||
description: "Sie versuchen, Ihre E-Mail-Adresse ändern: die neue E-Mail-Adresse wird zum Login sowie für Benachrichtigungen bzgl. Ihres Benutzerkontos verwendet."
|
||||
description: "Sie versuchen Ihre E-Mail-Adresse zu ändern: die neue E-Mail-Adresse wird zum Login sowie für Benachrichtigungen bzgl. Ihres Benutzerkontos verwendet."
|
||||
old_email: "Alte E-Mail-Adresse"
|
||||
new_email: "Neue E-Mail-Adresse"
|
||||
enter_password: "Passwort"
|
||||
@@ -362,7 +361,6 @@ de:
|
||||
required_field: "Dieses Feld ist erforderlich"
|
||||
done: "Fertig"
|
||||
content:
|
||||
title: "E-Mail-Adresse hinzufügen"
|
||||
description: "Aus Sicherheitsgründen benötigen wir eine E-Mail-Adresse zu jedem Benutzerkonto. Ihre E-Mail-Adresse wird für folgendes verwendet:"
|
||||
item_1: "Benachrichtigungen über Änderungen am Benutzerkonto (Nutzername, E-Mail-Adresse, Passwort)"
|
||||
item_2: "Ermöglicht den Download des eigenen Kommentar-Archivs"
|
||||
@@ -437,7 +435,6 @@ es:
|
||||
required_field: "Este campo es requerido"
|
||||
done: "Hecho"
|
||||
content:
|
||||
title: "Agregar una dirección de correo electrónico"
|
||||
description: "Para su seguridad adicional, solicitamos a los usuarios que agreguen una dirección de correo electrónico a sus cuentas. Su dirección de correo electrónico se usará para:"
|
||||
item_1: "Recibe actualizaciones sobre cualquier cambio en tu cuenta (dirección de correo electrónico, nombre de usuario, contraseña, etc.)"
|
||||
item_2: "Permitir que descargues tus comentarios."
|
||||
@@ -513,7 +510,6 @@ it:
|
||||
required_field: "Questo campo è obbligatorio"
|
||||
done: "Finito"
|
||||
content:
|
||||
title: "Aggiungi indirizzo email"
|
||||
description: "Per questioni di sicurezza, richiediamo agli utenti di aggiungere un indirizzo email associato ai loro account. Il tuo indirizzo email sarà usato per:"
|
||||
item_1: "Ricevere aggiornamenti riguardo modifiche relative al tuo account (indirizzo email, username, password, etc.)"
|
||||
item_2: "Permetterti di scaricare i tuoi commenti."
|
||||
@@ -589,7 +585,6 @@ nl_NL:
|
||||
required_field: "Dit veld is verplicht"
|
||||
done: "Klaar"
|
||||
content:
|
||||
title: "Voeg een e-mailadres toe"
|
||||
description: "Voor je veiligheid vragen we gebruikers om een e-mailadres toe te voegen aan hun account. Je e-mailadres zal worden gebruikt om:"
|
||||
item_1: "Updates te ontvangen omtrent wijzigingen in je account (e-mailadres, gebruikersnaam, wachtwoord, etc.)"
|
||||
item_2: "Je reacties te kunnen downloaden."
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: talk-plugin-moderator-actions
|
||||
permalink: /plugin/talk-plugin-moderator-actions/
|
||||
title: talk-plugin-moderation-actions
|
||||
permalink: /plugin/talk-plugin-moderation-actions/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-moderator-actions
|
||||
name: talk-plugin-moderation-actions
|
||||
default: true
|
||||
provides:
|
||||
- Client
|
||||
|
||||
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-category-featured/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-notifications-category-featured
|
||||
default: true
|
||||
depends:
|
||||
- name: talk-plugin-notifications
|
||||
- name: talk-plugin-featured-comments
|
||||
@@ -14,4 +15,4 @@ plugin:
|
||||
|
||||
When a comment is featured (via the
|
||||
[talk-plugin-featured-comments](/talk/plugin/talk-plugin-featured-comments)
|
||||
plugin), the user will receive a notification email.
|
||||
plugin), the user will receive a notification email.
|
||||
|
||||
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-moderation-actions/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-notifications-moderation-actions
|
||||
default: true
|
||||
depends:
|
||||
- name: talk-plugin-notifications
|
||||
provides:
|
||||
|
||||
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-category-reply/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-notifications-category-reply
|
||||
default: true
|
||||
depends:
|
||||
- name: talk-plugin-notifications
|
||||
provides:
|
||||
@@ -12,4 +13,4 @@ plugin:
|
||||
---
|
||||
|
||||
Replies made to each user will trigger an email to be sent with the notification
|
||||
details if enabled.
|
||||
details if enabled.
|
||||
|
||||
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-category-staff/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-notifications-category-staff
|
||||
default: true
|
||||
depends:
|
||||
- name: talk-plugin-notifications
|
||||
provides:
|
||||
|
||||
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-digest-daily/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-notifications-digest-daily
|
||||
default: true
|
||||
depends:
|
||||
- name: talk-plugin-notifications
|
||||
provides:
|
||||
@@ -13,4 +14,4 @@ plugin:
|
||||
|
||||
Enables a digesting option for users to digest their notifications on an `DAILY`
|
||||
basis, where the notification batching occurs every day at midnight in the
|
||||
`America/New_York` timezone.
|
||||
`America/New_York` timezone.
|
||||
|
||||
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications-digest-hourly/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-notifications-digest-hourly
|
||||
default: true
|
||||
depends:
|
||||
- name: talk-plugin-notifications
|
||||
provides:
|
||||
@@ -13,4 +14,4 @@ plugin:
|
||||
|
||||
Enables a digesting option for users to digest their notifications on an `HOURLY`
|
||||
basis, where the notification batching occurs every hour in the
|
||||
`America/New_York` timezone.
|
||||
`America/New_York` timezone.
|
||||
|
||||
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-notifications/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-notifications
|
||||
default: true
|
||||
provides:
|
||||
- Server
|
||||
- Client
|
||||
@@ -28,4 +29,4 @@ line by adjusting the translation, Talk's default behavior is to add a prefix
|
||||
before the subject of each email sent. This is always set to the
|
||||
[TALK-EMAIL-SUBJECT-PREFIX](/talk/advanced-configuration/#TALK-EMAIL-SUBJECT-PREFIX)
|
||||
configuration variable. You should change this parameter if you want to affect
|
||||
how the subject is rendered.
|
||||
how the subject is rendered.
|
||||
|
||||
@@ -4,11 +4,10 @@ permalink: /plugin/talk-plugin-offtopic/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-offtopic
|
||||
default: true
|
||||
provides:
|
||||
- Server
|
||||
- Client
|
||||
---
|
||||
|
||||
Allows the comment authors to tag their comment as `Off-Topic` which will add a
|
||||
visible badge on the frontend to other users that their comment is off-topic.
|
||||
visible badge on the frontend to other users that their comment is off-topic.
|
||||
|
||||
@@ -41,7 +41,7 @@ type CancelAccountDeletionResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# DownloadUserResponse contaisn the account download archiveURL that can be used
|
||||
# DownloadUserResponse contains the account download archiveURL that can be used
|
||||
# to directly download a zip file containing the user data.
|
||||
type DownloadUserResponse implements Response {
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ permalink: /plugin/talk-plugin-rich-text/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-rich-text
|
||||
default: true
|
||||
provides:
|
||||
- Client
|
||||
- Server
|
||||
|
||||
@@ -22,7 +22,7 @@ class Editor extends React.Component {
|
||||
};
|
||||
|
||||
getHTML(props = this.props) {
|
||||
if (props.input.richTextBody) {
|
||||
if (props.input.richTextBody !== undefined) {
|
||||
return props.input.richTextBody;
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,7 @@ plugin:
|
||||
Enables all new comments that are written to be posted to a Slack channel as
|
||||
well. Configure an
|
||||
[Incoming Webhook](https://api.slack.com/incoming-webhooks)
|
||||
app and provide that url in the form of the `SLACK_WEBHOOK_URL`
|
||||
app and provide that url in the form of the `TALK_SLACK_WEBHOOK_URL`
|
||||
detailed below.
|
||||
|
||||
*Warning: On high volume sites, this means every single comment will flow into
|
||||
@@ -20,5 +20,5 @@ recipe to further customize the behavior*.
|
||||
|
||||
Configuration:
|
||||
|
||||
- `SLACK_WEBHOOK_URL` (**required**) - The webhook url that will be
|
||||
used to post new comments to.
|
||||
- `TALK_SLACK_WEBHOOK_URL` (**required**) - The webhook url that will be
|
||||
used to post new comments to.
|
||||
|
||||
@@ -3,7 +3,7 @@ ar:
|
||||
label: الأكثر ردودً أولاً
|
||||
da:
|
||||
talk-plugin-sort-most-replied:
|
||||
label: Most replied first
|
||||
label: Most replied first
|
||||
de:
|
||||
talk-plugin-sort-most-replied:
|
||||
label: Häufigste Antworten zuerst
|
||||
@@ -15,7 +15,7 @@ es:
|
||||
label: Más respondidas primero
|
||||
fr:
|
||||
talk-plugin-sort-most-replied:
|
||||
label: Most replied first
|
||||
label: Les plus répondus en premier
|
||||
he:
|
||||
talk-plugin-sort-most-replied:
|
||||
label: הכי השיב קודם
|
||||
@@ -30,7 +30,7 @@ pt_BR:
|
||||
label: Mais respondidos primeiro
|
||||
sr:
|
||||
talk-plugin-sort-most-replied:
|
||||
label: Prvo sa najviše odgovora
|
||||
label: Prvo sa najviše odgovora
|
||||
zh_CN:
|
||||
talk-plugin-sort-most-replied:
|
||||
label: "最多回复在前"
|
||||
|
||||
@@ -15,7 +15,7 @@ es:
|
||||
label: Más respetadas primero
|
||||
fr:
|
||||
talk-plugin-sort-most-respected:
|
||||
label: Most respected first
|
||||
label: Les plus respectés en premier
|
||||
he:
|
||||
talk-plugin-sort-most-respected:
|
||||
label: הכי מכובד קודם
|
||||
|
||||
@@ -3,7 +3,7 @@ ar:
|
||||
label: الأحدث أولاً
|
||||
da:
|
||||
talk-plugin-sort-newest:
|
||||
label: Newest first
|
||||
label: Newest first
|
||||
de:
|
||||
talk-plugin-sort-newest:
|
||||
label: Neueste zuerst
|
||||
@@ -15,7 +15,7 @@ es:
|
||||
label: Más nuevas primero
|
||||
fr:
|
||||
talk-plugin-sort-newest:
|
||||
label: Newest first
|
||||
label: Les plus récents en premier
|
||||
he:
|
||||
talk-plugin-sort-newest:
|
||||
label: מהראשונה לאחרונה
|
||||
|
||||
@@ -3,7 +3,7 @@ ar:
|
||||
label: الأقدم أولاً
|
||||
da:
|
||||
talk-plugin-sort-oldest:
|
||||
label: Oldest first
|
||||
label: Oldest first
|
||||
de:
|
||||
talk-plugin-sort-oldest:
|
||||
label: Älteste zuerst
|
||||
@@ -15,7 +15,7 @@ es:
|
||||
label: Más viejas primero
|
||||
fr:
|
||||
talk-plugin-sort-oldest:
|
||||
label: Oldest first
|
||||
label: Les plus anciens en premier
|
||||
he:
|
||||
talk-plugin-sort-oldest:
|
||||
label: מהאחרונה לראשונה
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user