mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1385a0b961 | ||
|
|
7fd01e5845 | ||
|
|
340052cdf0 | ||
|
|
a085e4b6f9 | ||
|
|
1575b15e36 | ||
|
|
7da9126a76 | ||
|
|
9453d207f5 | ||
|
|
869c760a8b | ||
|
|
b283595c97 | ||
|
|
d672af63f9 | ||
|
|
b4ad78fd65 | ||
|
|
713de46c2a | ||
|
|
fc1e51ed62 | ||
|
|
4eef6f4218 |
@@ -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,6 +42,25 @@ 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 }) => {
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -3,33 +3,57 @@ 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.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 },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -43,17 +67,6 @@ export default function assets(state = initialState, action) {
|
||||
},
|
||||
},
|
||||
});
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -57,24 +55,34 @@ class StoriesContainer extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
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 +90,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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -359,7 +359,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'
|
||||
|
||||
+77
-2
@@ -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,6 +332,7 @@ 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'
|
||||
@@ -343,16 +376,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 +434,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 +472,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 +507,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 +542,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'
|
||||
|
||||
+1
-1
@@ -385,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'
|
||||
|
||||
+1
-1
@@ -369,7 +369,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'
|
||||
|
||||
+1
-1
@@ -372,7 +372,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"
|
||||
|
||||
+1
-1
@@ -333,7 +333,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'
|
||||
|
||||
+1
-1
@@ -371,7 +371,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'
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "4.9.0",
|
||||
"version": "4.10.0",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
|
||||
@@ -36,7 +36,7 @@ You can learn more about getting a Facebook App ID at the
|
||||
* add a link to your privacy policy
|
||||
* add a link to your terms of service
|
||||
* In Settings > Advanced:
|
||||
* turn on "Require App Secret"
|
||||
* disable "Require App Secret"
|
||||
* Add a "Product" (Under "Products" click + to add a Product):
|
||||
* Setup "Facebook Login"
|
||||
* choose `www`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
EmailAddressAdded,
|
||||
} from '../components/AddEmailAddress';
|
||||
|
||||
import styles from './AddEmailAddressDialog.css';
|
||||
|
||||
class AddEmailAddressDialog extends React.Component {
|
||||
state = {
|
||||
step: 0,
|
||||
@@ -72,7 +74,11 @@ class AddEmailAddressDialog extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Dialog open={true} id="talk-plugin-local-auth-email-dialog">
|
||||
<Dialog
|
||||
open={true}
|
||||
id="talk-plugin-local-auth-email-dialog"
|
||||
className={styles.dialog}
|
||||
>
|
||||
{step === 0 && <AddEmailForm onSubmit={this.handleSubmit} />}
|
||||
{step === 1 &&
|
||||
!requireEmailConfirmation && (
|
||||
|
||||
@@ -344,7 +344,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"
|
||||
|
||||
@@ -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: מהאחרונה לראשונה
|
||||
|
||||
@@ -7,7 +7,7 @@ da:
|
||||
talk-plugin-viewing-options:
|
||||
viewing_options: "Viewing Options"
|
||||
sort: Sorting
|
||||
filter: Filtering
|
||||
filter: Filtering
|
||||
de:
|
||||
talk-plugin-viewing-options:
|
||||
viewing_options: "Ansichtsoptionen"
|
||||
@@ -25,9 +25,9 @@ es:
|
||||
filter: Filtrado por
|
||||
fr:
|
||||
talk-plugin-viewing-options:
|
||||
viewing_options: "Viewing Options"
|
||||
sort: Sorting
|
||||
filter: Filtering
|
||||
viewing_options: "Options d'affichage"
|
||||
sort: Trier par
|
||||
filter: Filtrer par
|
||||
he:
|
||||
talk-plugin-viewing-options:
|
||||
viewing_options: "אפשרויות צפייה"
|
||||
|
||||
@@ -3,5 +3,6 @@ const router = express.Router();
|
||||
|
||||
// Return the current version.
|
||||
router.use('/v1', require('./v1'));
|
||||
router.use('/v2', require('./v2'));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const express = require('express');
|
||||
const { version } = require('../../../package.json');
|
||||
const { REVISION_HASH } = require('../../../config');
|
||||
const router = express.Router();
|
||||
|
||||
// Return the current version.
|
||||
router.get('/', (req, res) => {
|
||||
res.json({ version, revision: REVISION_HASH });
|
||||
});
|
||||
|
||||
router.use('/stories', require('./stories'));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,145 @@
|
||||
const express = require('express');
|
||||
const Joi = require('joi');
|
||||
const { get, first, last } = require('lodash');
|
||||
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const AssetModel = require('../../../models/asset');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const ListStorySchema = Joi.object({
|
||||
value: Joi.string()
|
||||
.empty('')
|
||||
.default(''),
|
||||
filter: Joi.string()
|
||||
.empty('')
|
||||
.valid(['all', 'open', 'closed'])
|
||||
.default('all'),
|
||||
limit: Joi.number()
|
||||
.empty('')
|
||||
.default(20)
|
||||
.max(500)
|
||||
.min(0),
|
||||
cursor: Joi.string()
|
||||
.empty('')
|
||||
.default(''),
|
||||
});
|
||||
|
||||
function validate(query) {
|
||||
const { value, error } = Joi.validate(query, ListStorySchema, {
|
||||
presence: 'optional',
|
||||
});
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authorization.needed('ADMIN', 'MODERATOR'),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
// Validate and extract the query arguments.
|
||||
let { cursor, value, filter, limit } = validate(req.query);
|
||||
const isTextBasedSearch = value.length > 0;
|
||||
|
||||
// The cursor can be a date or a number based on the style of search being
|
||||
// performed.
|
||||
cursor = Joi.attempt(
|
||||
cursor,
|
||||
isTextBasedSearch
|
||||
? Joi.number()
|
||||
.empty('')
|
||||
.min(0)
|
||||
.default(0)
|
||||
: Joi.date()
|
||||
.empty('')
|
||||
.default(null)
|
||||
);
|
||||
|
||||
// Create a new query to begin adding conditions.
|
||||
let query = AssetModel.find(
|
||||
{},
|
||||
isTextBasedSearch ? { score: { $meta: 'textScore' } } : {}
|
||||
);
|
||||
|
||||
if (filter === 'open') {
|
||||
// Filter by open stories.
|
||||
query.merge({
|
||||
$or: [{ closedAt: null }, { closedAt: { $gt: Date.now() } }],
|
||||
});
|
||||
} else if (filter === 'closed') {
|
||||
// Filter by closed stories.
|
||||
query.merge({
|
||||
closedAt: {
|
||||
$lt: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (isTextBasedSearch) {
|
||||
// This is a text based search, so search by the value.
|
||||
query.merge({
|
||||
$text: {
|
||||
$search: value,
|
||||
},
|
||||
});
|
||||
|
||||
// Sort by text search score.
|
||||
query.sort({ score: { $meta: 'textScore' } });
|
||||
|
||||
if (cursor) {
|
||||
// We are paginating, so we should skip stories based on the cursor.
|
||||
query.skip(cursor);
|
||||
}
|
||||
} else {
|
||||
// This is not a text based search, so sort by the created timestamp.
|
||||
query.sort({ created_at: -1 });
|
||||
|
||||
if (cursor) {
|
||||
// We are paginating, so we should sort based on the created
|
||||
// timestamp.
|
||||
query.merge({
|
||||
created_at: {
|
||||
$lt: cursor,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the query.
|
||||
const results = await query.limit(limit + 1);
|
||||
|
||||
const textTransformer = (node, idx) => ({
|
||||
node,
|
||||
cursor: idx + cursor + 1,
|
||||
});
|
||||
|
||||
const dateTransformer = node => ({
|
||||
node,
|
||||
cursor: node.created_at,
|
||||
});
|
||||
|
||||
// Slice the nodes to get only the requested number of elements.
|
||||
const edges = results
|
||||
.slice(0, limit)
|
||||
.map(isTextBasedSearch ? textTransformer : dateTransformer);
|
||||
|
||||
// Generate the pageInfo.
|
||||
const pageInfo = {
|
||||
startCursor: get(first(edges), 'cursor', null),
|
||||
endCursor: get(last(edges), 'cursor', null),
|
||||
hasNextPage: results.length > limit,
|
||||
};
|
||||
|
||||
// Send back the asset data.
|
||||
return res.json({ edges, pageInfo });
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user