Compare commits

...
36 Commits
Author SHA1 Message Date
Kim Gardner 1385a0b961 Bump version 4.10.0 (#2406) 2019-07-15 19:53:33 +01:00
Wyatt Johnson 7fd01e5845 [CORL-444] Stories Tab Adjustments (#2404)
* feat: improved stories tab

* feat: swapped date sorting with text sorting
2019-07-12 20:18:21 +00:00
immber 340052cdf0 DOCS | Add asset_url note to CMS integration & Update FB config (#2393)
* added note RE passing asset_url, and updated fb configs

* fix: syntax tweaks

* feat: small tweaks to embedding
2019-07-10 09:41:52 -07:00
Leandro a085e4b6f9 Translation key typo. (#2389)
Fix translation key "login.request_passowrd"
to "login.request_password".
2019-07-03 17:28:34 +01:00
Kim Gardner 1575b15e36 Merge pull request #2374 from leeeandroo/german-translations
Added missing German translations.
2019-06-21 15:32:55 +01:00
Kim Gardner 7da9126a76 Merge branch 'master' into german-translations 2019-06-21 15:24:17 +01:00
Kim Gardner 9453d207f5 Merge pull request #2373 from leeeandroo/featured-tooltip-breakword
Break headline on featured comments plugin
2019-06-21 15:23:18 +01:00
Leandro Vasco da Rocha 869c760a8b Grammatical changes on German wording 2019-06-20 14:44:52 +02:00
Leandro Vasco da Rocha b283595c97 Break headline on featured comments plugin.
If a headline is too long, we need to break it to fit the tooltip
content box.
2019-06-20 13:24:05 +02:00
Leandro Vasco da Rocha d672af63f9 Added missing german translations. 2019-06-18 15:53:27 +02:00
Kim Gardner b4ad78fd65 Bump version 4.9.1 (#2361) 2019-06-14 15:28:26 +00:00
Kim Gardner 713de46c2a Merge pull request #2359 from coralproject/corl-427
[CORL-427] AddEmailAddressDialog should appear at the top
2019-06-14 14:53:46 +01:00
Chi Vinh Le fc1e51ed62 fix: Position Add Email Address Dialog to the top 2019-06-14 00:08:37 +02:00
Etienne Martin 4eef6f4218 Added missing translations (#2357) 2019-06-13 19:06:41 +00:00
Kim Gardner c8cfd7c0f8 Merge pull request #2345 from coralproject/bump-4-9-0
Bump version 4.9.0
2019-06-06 20:11:11 +01:00
Kim Gardner 0e4cbf5f44 Bump version 4.9.0 2019-06-06 19:59:58 +01:00
Kim Gardner 448b988281 Merge pull request #2327 from coralproject/next-amp-docs
AMP docs
2019-06-05 20:55:40 +01:00
Kim Gardner 7a688495f2 Merge branch 'master' into next-amp-docs 2019-05-29 13:29:57 +01:00
Kim Gardner b9cf7084eb Merge pull request #2326 from Sentynel/master
Don't crash on email address lookup failure
2019-05-29 13:29:34 +01:00
Kim Gardner b99161f0cb Merge branch 'master' into next-amp-docs 2019-05-29 12:59:22 +01:00
Kim Gardner 6faa4862de Merge branch 'master' into master 2019-05-29 12:58:57 +01:00
Aram Zucker-Scharff eff3f1dfd8 Small correction of a spelling error (#2325) 2019-05-28 16:36:17 +00:00
immber 73aa77d7da CORL-380 | DOCS | Updates pre-launch checklist, asset scraping (#2317)
* updated asset scraping docs

* added note about setting close date

* added close window setting details
2019-05-27 17:58:18 +02:00
Chi Vinh Le 5580ab385f fix: remove margin from amp body 2019-05-27 17:20:07 +02:00
Chi Vinh Le 40eb0e97ec docs: add amp docs 2019-05-27 17:19:05 +02:00
Sam Lade b7e1ce0205 Don't crash on email address lookup failure
If for whatever reason an email job gets queued for a user with no
email, an exception is thrown on the getEmailAddress() step. This
exception was never handled and therefore the app crashed. Handle it and
log instead..
2019-05-27 14:12:14 +01:00
Kiwi bfde9bf8c7 [CORL-373] Experimental AMP Support (#2324)
* feat: support amp

* fix: support a custom PORT for amp development
2019-05-23 20:42:58 +02:00
Gerardo Gálvez 54f9b28086 Show/hide a comment's edit history from Moderation and User Detail. (#2299)
* Show/Hide comment edit history in moderation queues and user detail.

* Add spanish translations

* Place timestamp and comment body on separate lines

* Remove padding between timestamp and comment
2019-05-15 19:57:21 +02:00
immber 03a5eb7f9b Merge pull request #2307 from coralproject/fbdocs
DOCS | Update fb app creation steps
2019-05-09 15:51:51 -07:00
immber 43e60e6544 Merge branch 'master' into fbdocs 2019-05-09 15:37:24 -07:00
Jessie Rushing ebd16666e6 Merge branch 'fbdocs' of https://github.com/coralproject/talk into fbdocs 2019-05-09 15:36:37 -07:00
Jessie Rushing f2129082a9 added note about location of live button 2019-05-09 15:35:41 -07:00
immber 74226aa6fc Merge branch 'master' into fbdocs 2019-05-09 15:08:59 -07:00
Jessie Rushing b6ed5b792b updated Fb dev steps 2019-05-09 15:07:40 -07:00
Jessie Rushing 39e45c30ba added callback url 2019-05-08 16:05:13 -07:00
Jessie Rushing ed734ebb08 update fb app creation steps 2019-05-08 15:42:06 -07:00
57 changed files with 1125 additions and 220 deletions
+27 -13
View File
@@ -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 && (
+1 -1
View File
@@ -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 {
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
&nbsp;<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 -2
View File
@@ -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)}
+40 -27
View File
@@ -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,
@@ -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 {
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
&nbsp;<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,
});
};
@@ -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,
};
+41 -6
View File
@@ -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();
+1
View File
@@ -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) => {
+6
View File
@@ -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,
+2
View File
@@ -120,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
View File
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"hexo": {
"version": "3.7.1"
"version": "3.8.0"
},
"scripts": {
"start": "hexo serve",
+2 -1
View File
@@ -77,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 Talks 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/)
+5
View File
@@ -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
@@ -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/)
+47
View File
@@ -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 -2
View File
@@ -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.
+120 -52
View File
@@ -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 users 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 users 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 youre 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 youre 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”
Talks 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.
Talks 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.
+6 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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'
+4 -1
View File
@@ -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'
@@ -382,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'
+3
View File
@@ -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'
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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'
+3
View File
@@ -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,
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "4.8.7",
"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,
+30 -11
View File
@@ -11,23 +11,42 @@ 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.
**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!_
@@ -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"
@@ -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 {
@@ -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: "אפשרויות צפייה"
+1
View File
@@ -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;
+13
View File
@@ -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;
+145
View File
@@ -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;
+20
View File
@@ -0,0 +1,20 @@
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
return res.render('dev/amp.njk', {
title: 'Coral Talk AMP',
asset_url: '',
asset_id: '',
});
});
router.get('/button', (req, res) => {
return res.render('dev/amp-button.njk', {
title: 'Coral Talk AMP',
asset_url: '',
asset_id: '',
});
});
module.exports = router;
+1
View File
@@ -19,5 +19,6 @@ router.get('/', staticTemplate, async (req, res) => {
});
}
});
router.use('/amp', staticTemplate, require('./amp'));
module.exports = router;
+4
View File
@@ -5,4 +5,8 @@ router.use('/stream', (req, res) => {
res.render('embed/stream.njk');
});
router.use('/amp', (req, res) => {
res.render('embed/amp.njk');
});
module.exports = router;
+73
View File
@@ -0,0 +1,73 @@
<!-- ## Introduction -->
<!--
This is a sample showing how to use Talk with AMP. You need to access
this using an URL other than localhost. You can use ngrok to achieve that.
-->
<!-- -->
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<link rel="canonical" href="/dev">
<!-- ## Setup -->
<script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>
<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script>
<title>Coral Talk AMP</title>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<style amp-custom>
.container {
width: auto;
max-width: 680px;
padding: 0 15px;
margin: auto;
}
.title {
margin-top: 1rem;
margin-bottom: .5rem;
font-size: 2.5rem;
font-weight: 500;
line-height: 1.2;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
}
.hide{
display:none
}
</style>
</head>
<body>
<div class="container">
<h1 class="title">Coral Talk AMP</h1>
<p>
Ask to go outside and ask to come inside and ask to go outside and ask to
come inside the dog smells bad. Lick butt and make a weird face. Toilet
paper attack claws fluff everywhere meow miao french ciao litterbox. Shake
treat bag immediately regret falling into bathtub or white cat sleeps on a
black shirt so what a cat-ass-trophy! eat owner's food spit up on light
gray carpet instead of adjacent linoleum. Warm up laptop with butt lick
butt fart rainbows until owner yells pee in litter box hiss at cats
scratch the box so loved it, hated it, loved it, hated it but need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me.
</p>
<button id=menu on="tap:AMP.setState({visible: !visible})">Show Comments</button>
<div [class]=visible?"show":"hide" class="hide">
<amp-iframe
width=600 height=140
layout="responsive"
sandbox="allow-scripts allow-same-origin allow-modals allow-popups allow-forms"
resizable
src="http://localhost:3000/embed/amp">
<div placeholder></div>
<div overflow tabindex=0 role=button aria-label="Read more">Read more</div>
</amp-iframe>
</div<
</div>
</body>
</html>
+151
View File
@@ -0,0 +1,151 @@
<!-- ## Introduction -->
<!--
This is a sample showing how to use Talk with AMP. You need to access
this using an URL other than localhost. You can use ngrok to achieve that.
-->
<!-- -->
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<link rel="canonical" href="/dev">
<!-- ## Setup -->
<script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>
<title>Coral Talk AMP</title>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<style amp-custom>
.container {
width: auto;
max-width: 680px;
padding: 0 15px;
margin: auto;
}
.title {
margin-top: 1rem;
margin-bottom: .5rem;
font-size: 2.5rem;
font-weight: 500;
line-height: 1.2;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
}
</style>
</head>
<body>
<div class="container">
<h1 class="title">Coral Talk AMP</h1>
<p>
Dismember a mouse and then regurgitate parts of it on the family room
floor. Dont wait for the storm to pass, dance in the rain stand in front
of the computer screen, so stares at human while pushing stuff off a table
chew the plant meow hiss at vacuum cleaner. Terrorize the
hundred-and-twenty-pound rottweiler and steal his bed, not sorry chew the
plant. Litter kitter kitty litty little kitten big roar roar feed me rub
whiskers on bare skin act innocent sleep on keyboard, so give me attention
or face the wrath of my claws for demand to be let outside at once, and
expect owner to wait for me as i think about it spread kitty litter all
over house so nya nya nyan. Catty ipsum massacre a bird in the living room
and then look like the cutest and most innocent animal on the planet you
have cat to be kitten me right meow. Hiss and stare at nothing then run
suddenly away refuse to come home when humans are going to bed; stay out
all night then yowl like i am dying at 4am and lick plastic bags. Chase
dog then run away purrr purr littel cat, little cat purr purr and step on
your keyboard while you're gaming and then turn in a circle . Twitch tail
in permanent irritation put butt in owner's face and the dog smells bad
yet attempt to leap between furniture but woefully miscalibrate and
bellyflop onto the floor; what's your problem? i meant to do that now i
shall wash myself intently. Sniff all the things groom forever, stretch
tongue and leave it slightly out, blep, but bring your owner a dead bird
decide to want nothing to do with my owner today for lay on arms while
you're using the keyboard meow meow, i tell my human or scratch. Sleep on
my human's head then cats take over the world bleghbleghvomit my furball
really tie the room together sleep more napping, more napping all the
napping is exhausting. When in doubt, wash drink water out of the faucet,
cats are fats i like to pets them they like to meow back and cat dog hate
mouse eat string barf pillow no baths hate everything yet swat at dog
kitty kitty but you call this cat food. Cough furball into food bowl then
scratch owner for a new one flex claws on the human's belly and purr like
a lawnmower for has closed eyes but still sees you groom yourself 4 hours
- checked, have your beauty sleep 18 hours - checked, be fabulous for the
rest of the day - checked. Freak human out make funny noise mow mow mow
mow mow mow success now attack human flex claws on the human's belly and
purr like a lawnmower or meowwww. Terrorize the hundred-and-twenty-pound
rottweiler and steal his bed, not sorry paw at your fat belly so yowling
nonstop the whole night small kitty warm kitty little balls of fur or eat
owner's food reward the chosen human with a slow blink. Gate keepers of
hell plan steps for world domination for more napping, more napping all
the napping is exhausting give me some of your food give me some of your
food give me some of your food meh, i don't want it so flop over. Make
meme, make cute face ears back wide eyed so sit and stare. Dead stare with
ears cocked furrier and even more furrier hairball. Stand in front of the
computer screen demand to have some of whatever the human is cooking, then
sniff the offering and walk away for catasstrophe, kitty scratches couch
bad kitty. Wack the mini furry mouse intrigued by the shower, and pooping
rainbow while flying in a toasted bread costume in space. Mesmerizing
birds love me! shake treat bag, yet lies down where is my slave? I'm
getting hungry so lick face hiss at owner, pee a lot, and meow repeatedly
scratch at fence purrrrrr eat muffins and poutine until owner comes back.
You have cat to be kitten me right meow sniff other cat's butt and hang
jaw half open thereafter but run outside as soon as door open so munch on
tasty moths or munch on tasty moths, for paw at beetle and eat it before
it gets away. Sit on human. Gnaw the corn cob massacre a bird in the
living room and then look like the cutest and most innocent animal on the
planet for sit on the laptop. Meow scratch leg; meow for can opener to
feed me cat fur is the new black but hide when guests come over, and Gate
keepers of hell. Refuse to come home when humans are going to bed; stay
out all night then yowl like i am dying at 4am cat slap dog in face or eat
a rug and furry furry hairs everywhere oh no human coming lie on counter
don't get off counter for i like fish sit on human they not getting up
ever but meow meow but cuddle no cuddle cuddle love scratch scratch.
</p>
<p>
I show my fluffy belly but it's a trap! if you pet it i will tear up your
hand refuse to drink water except out of someone's glass mice, so cough
hairball, eat toilet paper or curl into a furry donut lick sellotape but
wack the mini furry mouse. When owners are asleep, cry for no apparent
reason. Chase imaginary bugs. Stinky cat reward the chosen human with a
slow blink, or chase dog then run away. Chew on cable scratch the
furniture for you are a captive audience while sitting on the toilet, pet
me for i like cats because they are fat and fluffy and spend all night
ensuring people don't sleep sleep all day. Scoot butt on the rug need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me, leave fur on owners clothes, so instantly
break out into full speed gallop across the house for no reason play
riveting piece on synthesizer keyboard and scoot butt on the rug yet meow
meow. Attack dog, run away and pretend to be victim annoy the old grumpy
cat, start a fight and then retreat to wash when i lose or meow go back to
sleep owner brings food and water tries to pet on head, so scratch get
sprayed by water because bad cat. Meowwww pelt around the house and up and
down stairs chasing phantoms drink water out of the faucet meow meow, i
tell my human. Destroy couch.
</p>
<p>
Ask to go outside and ask to come inside and ask to go outside and ask to
come inside the dog smells bad. Lick butt and make a weird face. Toilet
paper attack claws fluff everywhere meow miao french ciao litterbox. Shake
treat bag immediately regret falling into bathtub or white cat sleeps on a
black shirt so what a cat-ass-trophy! eat owner's food spit up on light
gray carpet instead of adjacent linoleum. Warm up laptop with butt lick
butt fart rainbows until owner yells pee in litter box hiss at cats
scratch the box so loved it, hated it, loved it, hated it but need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me.
</p>
<amp-iframe
width=600 height=140
layout="responsive"
sandbox="allow-scripts allow-same-origin allow-modals allow-popups allow-forms"
resizable
src="http://localhost:{{ PORT }}/embed/amp">
<div placeholder></div>
<div overflow tabindex=0 role=button aria-label="Read more">Read more</div>
</amp-iframe>
</div>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>Coral Talk Amp Embed</title>
<style>body { margin: 0; }</style>
</head>
<body>
<div id='coralStreamEmbed'></div>
<script src="{{ resolve('embed.js') }}"></script>
<script>
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
talk: '{{ BASE_URL }}',
auth_token: '',
amp: true,
})
</script>
</body>
</html>