mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
620273769f | ||
|
|
dff3d79569 | ||
|
|
0904453e41 | ||
|
|
90eafb557c | ||
|
|
4dbd1e0bb3 | ||
|
|
cbda970437 | ||
|
|
eecd814e53 | ||
|
|
fb4b6330da | ||
|
|
eb24b5c027 | ||
|
|
3346f5de77 | ||
|
|
1a69cea9f4 | ||
|
|
d763f0b8f6 | ||
|
|
6bb98a00dc | ||
|
|
2edb8e8d0a | ||
|
|
c3e162dd56 | ||
|
|
3cdd061afd | ||
|
|
aaaf37b8db | ||
|
|
7af5f04e45 | ||
|
|
65a166b860 | ||
|
|
eb4bcb71c4 | ||
|
|
6a04b66d84 | ||
|
|
30ae9a82c1 | ||
|
|
ec06f2a392 | ||
|
|
f461f0884b | ||
|
|
d55c329f76 | ||
|
|
0f63e076a6 | ||
|
|
1012b6b28b | ||
|
|
421352c75c | ||
|
|
a170c107e2 | ||
|
|
104617d644 | ||
|
|
5b1773ba26 | ||
|
|
cd0aeca87c | ||
|
|
5c65702032 | ||
|
|
66445aae59 | ||
|
|
23156f3d5e |
@@ -1,4 +1,4 @@
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946) · [](CONTRIBUTING.md#pull-requests)
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](CONTRIBUTING.md#pull-requests)
|
||||
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/talk).
|
||||
|
||||
|
||||
@@ -17,19 +17,19 @@ export const checkLogin = () => (
|
||||
if (!result.user) {
|
||||
cleanAuthData(localStorage);
|
||||
dispatch(checkLoginSuccess(null));
|
||||
client.resetWebsocket();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
client.resetWebsocket();
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status && error.status === 401 && localStorage) {
|
||||
// Unauthorized.
|
||||
cleanAuthData(localStorage);
|
||||
client.resetWebsocket();
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import {
|
||||
ENABLE_PLUGINS_DEBUG,
|
||||
DISABLE_PLUGINS_DEBUG,
|
||||
} from '../constants/config';
|
||||
import { LOGOUT } from '../constants/auth';
|
||||
import { LOGOUT, SET_AUTH_TOKEN } from '../constants/auth';
|
||||
|
||||
const initialState = {};
|
||||
const initialState = {
|
||||
auth_token: null,
|
||||
};
|
||||
|
||||
export default function config(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
@@ -30,6 +32,11 @@ export default function config(state = initialState, action) {
|
||||
...state,
|
||||
auth_token: null,
|
||||
};
|
||||
case SET_AUTH_TOKEN:
|
||||
return {
|
||||
...state,
|
||||
auth_token: action.token || null,
|
||||
};
|
||||
case MERGE_CONFIG:
|
||||
return {
|
||||
...state,
|
||||
|
||||
+4
-8
@@ -42,17 +42,13 @@ const getAuthToken = (store, storage) => {
|
||||
let state = store.getState();
|
||||
|
||||
if (state.config && state.config.auth_token) {
|
||||
// if an auth_token exists in config, use it.
|
||||
return state.config.auth_token;
|
||||
// If the embed is called with `embed.login(token)`, and the browser is not
|
||||
// capable of storing the token in localStorage, then we would have
|
||||
// persisted it to the redux state.
|
||||
return state.config.auth_token || state.auth.token;
|
||||
} else if (!bowser.safari && !bowser.ios && storage) {
|
||||
// Use local storage auth tokens where there's a stable api.
|
||||
return storage.getItem('token');
|
||||
} else if (state.auth && state.auth.token) {
|
||||
// Use the redux token state if the remaining methods fall out. If the embed
|
||||
// is called with `embed.login(token)`, and the browser is not capable of
|
||||
// storing the token in localStorage, then we would have persisted it to the
|
||||
// redux state.
|
||||
return state.auth.token;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -75,10 +75,15 @@ let TIMEAGO_INSTANCE;
|
||||
// detectLanguage will try to get the locale from storage if available,
|
||||
// otherwise will try to get it from the navigator, otherwise, it will fallback
|
||||
// to the default language.
|
||||
const detectLanguage = () =>
|
||||
first(
|
||||
const detectLanguage = () => {
|
||||
var browserLanguages = navigator.languages;
|
||||
//IE11 and MS-EDGE do not provide navigator.languages
|
||||
if (!browserLanguages) {
|
||||
browserLanguages = [navigator.language];
|
||||
}
|
||||
return first(
|
||||
negotiateLanguages(
|
||||
navigator.languages,
|
||||
browserLanguages,
|
||||
whitelistedLanguages || supportedLocales,
|
||||
{
|
||||
defaultLocale,
|
||||
@@ -86,6 +91,7 @@ const detectLanguage = () =>
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export function setupTranslations() {
|
||||
// locale
|
||||
|
||||
+9
-1
@@ -102,7 +102,7 @@ sidebar:
|
||||
children:
|
||||
- title: Authentication
|
||||
url: /integrating/authentication/
|
||||
- title: Asset Managment
|
||||
- title: Asset Management
|
||||
url: /integrating/asset-management/
|
||||
- title: Configuring the Comment Stream
|
||||
url: /integrating/configuring-comment-stream/
|
||||
@@ -164,10 +164,18 @@ sidebar:
|
||||
url: /when-youve-installed-talk/
|
||||
- title: Migrating
|
||||
children:
|
||||
- title: Migrating from v3.x.x
|
||||
url: /migration/3/
|
||||
- title: Migrating to v4.0.0
|
||||
url: /migration/4/
|
||||
- title: Migrating to v4.1.0
|
||||
url: /migration/4.1/
|
||||
- title: FAQ & Troubleshooting
|
||||
children:
|
||||
- title: FAQ
|
||||
url: /faq/
|
||||
- title: Troubleshooting Tips
|
||||
url: /troubleshooting-tips/
|
||||
- title: Contact
|
||||
url: /contact/
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Migrating from v3.x.x
|
||||
permalink: /migration/3/
|
||||
---
|
||||
|
||||
## Deprecation Notices
|
||||
|
||||
It was previously recommended to use the user service function:
|
||||
|
||||
```js
|
||||
Users.findOrCreateExternalUser(...);
|
||||
```
|
||||
|
||||
If you are developing a social plugin, you should migrate this function to:
|
||||
|
||||
```js
|
||||
Users.upsertSocialUser(...);
|
||||
```
|
||||
|
||||
If you are developing an external auth integration (where the integration
|
||||
provides) a custom displayName, you should migrate to:
|
||||
|
||||
```js
|
||||
Users.upsertExternalUser(...);
|
||||
```
|
||||
|
||||
## Troubleshooting Username Status
|
||||
|
||||
You may be affected by a side-effect of the above mentioned deprecated function
|
||||
`Users.findOrCreateExternalUser(...);` if the following are true:
|
||||
|
||||
1. You have upgraded from Talk `< 3` to `>= 4` and have completed a database
|
||||
migration
|
||||
2. You have used a custom auth plugin in the past
|
||||
3. You have disabled or not included the `talk-plugin-auth` as a `client` plugin
|
||||
4. You have received reports that some users can not comment, and are instead
|
||||
given a message `You are not authorized to perform this action.`
|
||||
|
||||
If this is the case, you can execute the following one time MongoDB query to
|
||||
repair the affected users.
|
||||
|
||||
```js
|
||||
db.users.update(
|
||||
{
|
||||
"status.username.status": {
|
||||
$in: ["UNSET", "CHANGED"]
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
"status.username.status": "SET"
|
||||
},
|
||||
$push: {
|
||||
"status.username.history": {
|
||||
status: "SET",
|
||||
assigned_by: null,
|
||||
created_at: ISODate()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
multi: true
|
||||
}
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
**Note: You must resolve and/or update your custom auth code to resolve the
|
||||
above mentioned deprecation notices _before_ running the above mentioned MongoDB
|
||||
query**
|
||||
@@ -3,15 +3,67 @@ title: FAQ
|
||||
permalink: /faq/
|
||||
---
|
||||
|
||||
## How can I get help integrating Talk into my newsroom?
|
||||
## How can I get help, submit a bug, or suggest a feature?
|
||||
|
||||
We're here to help with newsrooms of all sizes. Email our Integration Engineer
|
||||
([jeff@mozillafoundation.org](mailto:jeff@mozillafoundation.org)) to set up a meeting.
|
||||
There are a few avenues to get in touch with us and others in the community for help.
|
||||
|
||||
## How do I request a feature or submit a bug?
|
||||
To log a bug or request a feature, submit a Support ticket ([support@coralproject.net](mailto:support@coralproject.net)) and someone from our team will get back to you.
|
||||
|
||||
The best way is to [submit a Github issue](https://github.com/coralproject/talk/issues). Make sure you give plenty of details, our Core Team can usually respond within a few hours on weekdays.
|
||||
You can also request help on Github by [submitting an issue](https://github.com/coralproject/talk/issues). This also increases your chances of having someone from the community respond to help.
|
||||
|
||||
And you can also search our [Coral Community](https://community.coralproject.net) to see if your issue has been solved, or to get tips from the community.
|
||||
|
||||
## How can our dev team contribute to Talk?
|
||||
|
||||
We are lucky to work with newsroom dev teams and individual contributors who span the world, and come from newsrooms of all sizes. You can read our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md) to get started, but feel free to reach out to us via Github, or get in touch directly with Jeff via jeff@mozillafoundation.org.
|
||||
We are lucky to work with newsroom dev teams and individual contributors who span the world, and come from newsrooms of all sizes. You can read our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md) to get started, but feel free to reach out to us via Github too.
|
||||
|
||||
## What if we want to add a feature you don't have?
|
||||
|
||||
Talk is open source, so you're free to develop additional functionality and [submit a pull request](https://github.com/coralproject.net/talk).
|
||||
|
||||
## Do you have GDPR features?
|
||||
|
||||
Yes! Please read our [GDPR documentation](/talk/integrating/gdpr/) for more information and instructions to get started.
|
||||
|
||||
## Can I import my existing comments?
|
||||
|
||||
Yes! We have a community-supported [import framework](https://github.com/coralproject/talk-importer) that you can use to migrate your existing comments.
|
||||
|
||||
## What support is available?
|
||||
|
||||
Our team is small, so it's difficult for us to provide support packages. However, you can always email us at [support@coralproject.net](mailto:support@coralproject.net), and we can help answer your questions. In some cases, we can provide premium support packages either with our team, or through partners. You can inquire about this via the support email address above.
|
||||
|
||||
## Is there a hosted version I can purchase by monthly subscription?
|
||||
|
||||
Yes! We are happy to announce that as of July 2018, we provide a SaaS version of Talk, called the Coral Cloud. For a monthly subscription, you get your own hosted Talk instance to embed on your news and blog articles. [Reach out to us](mailto:support@coralproject.net) if you're interested in this option.
|
||||
|
||||
## Where is our data when we use Talk?
|
||||
|
||||
If you are hosting Talk on your own:
|
||||
|
||||
* Your data is stored in a MongoDB database that you provide
|
||||
* The Coral Team doesn’t have any access to your data
|
||||
|
||||
If you are using Coral Cloud Hosting for Talk:
|
||||
|
||||
* Your data is stored in a dedicated MongDB database that is provisioned for your in the Cloud
|
||||
* Your data is completely isolated from other customer’s data
|
||||
* The Coral Team and its third party database hosting providers use strict access controls and auditing to protected your data from unauthorized access by team members
|
||||
|
||||
## Does Talk have any automated moderation features to protect against spam and trolling?
|
||||
|
||||
Talk features a couple of plugins that provide advanced moderation:
|
||||
|
||||
* The [Toxic Comments plugin](/talk/plugin/talk-plugin-toxic-comments) integrates with the [Perspective API from Google](https://www.perspectiveapi.com/) to detect the likelihood of toxicity of comments in real-time
|
||||
* The [Akismet plugin](/talk/plugin/talk-plugin-akismet) detects and blocks spam comments
|
||||
|
||||
## How much can I customize Talk?
|
||||
|
||||
* The CSS of the Talk comment stream can be customized by [adding your own CSS via an external stylesheet](/talk/integrating/styling-css/)
|
||||
* The functionality of Talk can be [extended through the plugin framework](/talk/plugins/)
|
||||
|
||||
## How much does Talk cost?
|
||||
|
||||
* The Talk software is freely available under the Apache 2.0 open source license
|
||||
* Associated costs are those for the infrastructure required to run Talk (i.e. cloud hosting fee or bare-metal server costs)
|
||||
* The Coral Project offers a SaaS version of Talk. Please [get in touch with us](mailto:support@coralproject.net) to discuss pricing for your custom requirements.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Troubleshooting Tips
|
||||
permalink: /troubleshooting-tips/
|
||||
---
|
||||
|
||||
## How do I find out what version I'm running?
|
||||
|
||||
If you visit https://<YOUR TALK INSTANCE>/api/v1, it will return the version you're running and the hash for the latest commit.
|
||||
|
||||
## I've installed Talk but I can't see the comment stream appear on my articles
|
||||
|
||||
* Make sure you've adding the correct domains to your Permitted Domains in Configure > Tech Settings
|
||||
* Make sure you've correctly added the embed via your CMS to your article pages
|
||||
* Check the console for any errors and you can file a bug via [support](mailto:support@coralproject.net) if you can't resolve the issue
|
||||
|
||||
## My commenters are reporting they can't see the comment stream, but I'm able to
|
||||
|
||||
* If this seems to be isolated to one commenter, it could be related to 3rd party cookies
|
||||
* Post 4.x Talk doesn't require 3rd party cookies to be enabled. Check the version of Talk you're using and you might consider upgrading.
|
||||
* A quick fix in the meantime is to ask them to allow 3rd party cookies
|
||||
* You could also try asking them to clear their browser cache
|
||||
|
||||
|
||||
## My commenters are reporting that they cannot login to Talk
|
||||
|
||||
If you're using your own custom auth plugin:
|
||||
|
||||
* Review the code and your server logs to ensure your plugin is working correctly. Check [our auth docs](/talk/integrating/authentication/) for more tips.
|
||||
* Ensure that your JWT token settings, especially expiry, is being set correctly. You can troubleshoot JWT related issues with the [JWT Debugger](https://jwt.io/).
|
||||
* See if you can isolate if it's a particular group of users that are experiencing this issue, e.g. mods, admins, subscribers? Confirm they have the appropriate permissions to comment.
|
||||
* Note if this is a new issue that happened after an upgrade - did you read the [migration docs](/talk/migration/3/) and the [release notes](https://github.com/coralproject/talk/releases)? This might help you resolve the issue
|
||||
* Confirm that users who are affected have the correct `username.status`. If users have status `UNSET`, this is related to a bug with upgrading from 3.x to 4.x that has affected some organizations. Read more here about [upgrading from 3.x to 4.x](/talk/migration/3/).
|
||||
* If you're still experiencing issues, log a [support ticket](mailto:support@coralproject.net) so we can help diagnose the issue
|
||||
|
||||
|
||||
If you're using `talk-plugin-auth`:
|
||||
|
||||
* See if you can isolate if it's a particular group of users that are experiencing this issue, e.g. mods, admins, subscribers? Confirm they have the appropriate permissions to comment.
|
||||
* Note if this is a new issue that happened after an upgrade - did you read the [migration docs](/talk/migration/3/) and the [release notes](https://github.com/coralproject/talk/releases)? This might help you resolve the issue.
|
||||
* If you're still experiencing issues, log a [support ticket](mailto:support@coralproject.net) so we can help diagnose the issue
|
||||
@@ -22,7 +22,7 @@ Feel free to check all the utilities here: `talk/plugin-api`.
|
||||
|
||||
#### Stream
|
||||
* `setSort`
|
||||
* `showSignInDialog``
|
||||
* `showSignInDialog`
|
||||
|
||||
### Import
|
||||
```
|
||||
@@ -264,7 +264,7 @@ Coral UI is a set of components to help you build your UI. This powers our core.
|
||||
|
||||
### Import
|
||||
```js
|
||||
import {Button} from 'plugin-api/beta/components/ui';
|
||||
import {Button} from 'plugin-api/beta/client/components/ui';
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
@@ -319,7 +319,7 @@ module.exports = {
|
||||
let user;
|
||||
try {
|
||||
const { id, provider, displayName } = profile;
|
||||
user = await UsersService.findOrCreateExternalUser(
|
||||
user = await UsersService.upsertSocialUser(
|
||||
req.context,
|
||||
id,
|
||||
provider,
|
||||
|
||||
@@ -7,7 +7,7 @@ Using plugins and configuration variables, you can modify the way the Admin look
|
||||
|
||||
### Creating a Custom Moderation Queue
|
||||
|
||||
Talk can support custom pluggable mod queues, meaning you can write a plugin that has some logic and determines which comments should appear there. This works by adding a field modQueues` in the `index.js` of your client side plugin, like so:
|
||||
Talk can support custom pluggable mod queues, meaning you can write a plugin that has some logic and determines which comments should appear there. This works by adding a field `modQueues` in the `index.js` of your client side plugin, like so:
|
||||
|
||||
```
|
||||
modQueues: {
|
||||
|
||||
@@ -8,3 +8,300 @@ You can add your own stylesheet in Admin > Configure > Tech Settings.
|
||||
If you would like to change the styling of any elements in Talk, we provide global classnames with the prefix `talk-`. The easiest way to find the classname for the element you're looking for is to use the web inspector, and then update your stylesheet accordingly.
|
||||
|
||||
Plugins also have their own stylesheets located in the client directory.
|
||||
|
||||
|
||||
Here is an example stylesheet that we use on our [Coral Blog](https://coralproject.net/blog):
|
||||
|
||||
```css
|
||||
/*
|
||||
* You can use this stylesheet as a place to get started
|
||||
* for styling your own version of Talk!
|
||||
* Author: Sam Hankins, Coral Project, 2018
|
||||
* License: Apache 2.0
|
||||
*/
|
||||
|
||||
* {
|
||||
/* font-family: inherit; */
|
||||
}
|
||||
|
||||
html, body {
|
||||
width:auto;
|
||||
height:auto;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
margin: 0px;
|
||||
padding: 0px 0px 100px 0px;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#talk-embed-stream-container {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.expandForSignin {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.coralButton {
|
||||
margin: 5px 10px 5px 0px;
|
||||
background: none;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.coralButton:hover {
|
||||
border-radius: 2px;
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
.coralButton i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.coralHr {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.screen-reader-text {
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
/* Notification styles */
|
||||
#coral-notif {
|
||||
position: fixed;
|
||||
border: 0;
|
||||
background: rgb(105,105,105);
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Info Box Styles */
|
||||
|
||||
|
||||
.talk-plugin-infobox-info {
|
||||
top: 0;
|
||||
border: 0;
|
||||
background: #DEEDFF;
|
||||
color: #2a2a2a;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.talk-plugin-infobox-info em{
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.talk-plugin-infobox-info strong{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.talk-plugin-infobox-info blockquote{
|
||||
border-left: solid 2px #2a2a2a;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
.talk-plugin-infobox-info a{
|
||||
color: #2a2a2a;
|
||||
}
|
||||
|
||||
/* Question Box Styles */
|
||||
|
||||
.talk-stream-comments-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Comment styles */
|
||||
.comment {
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.talk-plugin-commentcontent-text {
|
||||
margin-bottom: 7px;
|
||||
font-size: 16px;
|
||||
font-weight: 100;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Tag Labels */
|
||||
|
||||
.talk-plugin-tag-label {
|
||||
background-color: #4C1066;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
padding: 5px 6px;
|
||||
}
|
||||
|
||||
/* Comment Action Styles */
|
||||
|
||||
.commentActionsRight, .replyActionsRight {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.commentActionsLeft, .replyActionsLeft {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
float: left;
|
||||
}
|
||||
|
||||
button.comment__action-button,
|
||||
.comment__action-button button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.comment__action-button[disabled],
|
||||
.comment__action-button[disabled] button {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.comment__action-button--nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.likedButton {
|
||||
color: rgb(0,134,227);
|
||||
}
|
||||
|
||||
.flaggedIcon {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
/* Flag Styles */
|
||||
|
||||
.talk-plugin-flags-popup-form {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.talk-plugin-flags-popup-header {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.talk-plugin-flags-popup-radio {
|
||||
margin:5px;
|
||||
}
|
||||
|
||||
.talk-plugin-flags-popup-radio-label {
|
||||
margin:5px;
|
||||
font-weight: 400;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.talk-plugin-flags-popup-counter {
|
||||
float: left;
|
||||
margin-top: 21px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.talk-plugin-flags-popup-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.talk-plugin-flags-reason-text {
|
||||
margin-left: 20px;
|
||||
margin-top: 5px;
|
||||
width: 75%;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
/* Close comments */
|
||||
|
||||
.close-comments-message {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.close-comments-confirm-wrapper {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.close-comments-alert {
|
||||
background-color: #d65344;
|
||||
color: white;
|
||||
font-size: 1.33rem;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.close-comments-alert i.material-icons {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
/* Load More */
|
||||
|
||||
.talk-load-more {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.talk-load-more button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
background-color: #2376D8;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
line-height: 1em;
|
||||
text-transform: capitalize;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.talk-load-more:hover button {
|
||||
background-color: #4399FF;
|
||||
}
|
||||
|
||||
.talk-new-comments {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.talk-load-more-replies {
|
||||
width: 100%;
|
||||
padding-left: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.talk-load-more-replies .talk-load-more-button {
|
||||
background-color: transparent;
|
||||
color: #979797;
|
||||
border: #979797 solid 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.talk-load-more-replies .talk-load-more:hover button {
|
||||
background-color: #979797;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -84,10 +84,10 @@ const getUsersByQuery = async (
|
||||
|
||||
if (value.length > 0) {
|
||||
// Lowercase the search term and escape any regex characters.
|
||||
value = escapeRegExp(value).toLowerCase();
|
||||
value = escapeRegExp(value);
|
||||
|
||||
// Compile the prefix search regex.
|
||||
const $regex = new RegExp(`^${value}`);
|
||||
const lowercasedRegex = new RegExp(`^${value.toLowerCase()}`);
|
||||
const notLowercasedRegex = new RegExp(`^${value}`);
|
||||
|
||||
// Merge in the regex params.
|
||||
query.merge({
|
||||
@@ -95,7 +95,7 @@ const getUsersByQuery = async (
|
||||
// Search by a prefix match on the username.
|
||||
{
|
||||
lowercaseUsername: {
|
||||
$regex,
|
||||
$regex: lowercasedRegex,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -104,7 +104,7 @@ const getUsersByQuery = async (
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: {
|
||||
$regex,
|
||||
$regex: lowercasedRegex,
|
||||
},
|
||||
provider: 'local',
|
||||
},
|
||||
@@ -114,7 +114,7 @@ const getUsersByQuery = async (
|
||||
// Search by the displayName metadata field.
|
||||
{
|
||||
'metadata.displayName': {
|
||||
$regex,
|
||||
$regex: notLowercasedRegex,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -293,7 +293,16 @@ const setStatus = async (ctx, { id, status }) => {
|
||||
*/
|
||||
const editComment = async (
|
||||
ctx,
|
||||
{ id, asset_id, edit: { body, metadata = {} } }
|
||||
{
|
||||
id,
|
||||
asset_id,
|
||||
edit: {
|
||||
body,
|
||||
metadata = {},
|
||||
status: commentStatus,
|
||||
actions: commentActions = [],
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const {
|
||||
connectors: {
|
||||
@@ -303,7 +312,13 @@ const editComment = async (
|
||||
|
||||
// Build up the new comment we're setting. We need to check this with
|
||||
// moderation now.
|
||||
let comment = { id, asset_id, body };
|
||||
let comment = {
|
||||
id,
|
||||
asset_id,
|
||||
body,
|
||||
status: commentStatus,
|
||||
actions: commentActions,
|
||||
};
|
||||
|
||||
// Determine the new status of the comment.
|
||||
const { actions, status } = await Moderation.process(ctx, comment);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
const { SubscriptionManager } = require('graphql-subscriptions');
|
||||
const { SubscriptionServer } = require('subscriptions-transport-ws');
|
||||
const debug = require('debug')('talk:graph:subscriptions');
|
||||
const DataLoader = require('dataloader');
|
||||
|
||||
const { getPubsub } = require('./pubsub');
|
||||
const schema = require('../schema');
|
||||
const Context = require('../context');
|
||||
const plugins = require('../../services/plugins');
|
||||
const User = require('../../models/user');
|
||||
const { singleJoinBy } = require('../loaders/util');
|
||||
|
||||
const { deserializeUser } = require('../../services/subscriptions');
|
||||
const setupFunctions = require('./setupFunctions');
|
||||
@@ -59,31 +62,103 @@ const onConnect = async (connectionParams, connection) => {
|
||||
}`;
|
||||
}
|
||||
|
||||
try {
|
||||
// Pull the user off of the upgrade request.
|
||||
const hydratedRequest = await deserializeUser(connection.upgradeReq);
|
||||
|
||||
// Update the connections upgrade request, as we'll use that to verify that
|
||||
// the user is allowed each operation.
|
||||
connection.upgradeReq = hydratedRequest;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// Call all the hooks.
|
||||
await Promise.all(
|
||||
hooks.onConnect.map(hook => hook(connectionParams, connection))
|
||||
);
|
||||
};
|
||||
|
||||
const onOperation = (parsedMessage, baseParams, connection) => {
|
||||
// Cache the upgrade request.
|
||||
let upgradeReq = connection.upgradeReq;
|
||||
/**
|
||||
* batchedUserRefresher will get users based on ID for websocket user refresh
|
||||
* operations to reduce load related to user refreshing.
|
||||
*/
|
||||
const batchedUserRefresher = new DataLoader(
|
||||
userIDs => {
|
||||
console.log(`OPERATION: refreshing ${userIDs.length} users.`);
|
||||
return User.find({ id: { $in: userIDs } }).then(
|
||||
singleJoinBy(userIDs, 'id')
|
||||
);
|
||||
},
|
||||
{
|
||||
// Disable the cache, as this dataloader is long lived, and the point of
|
||||
// using this dataloader is to batch refetch operations rather than caching
|
||||
// then as we normally would.
|
||||
cache: false,
|
||||
}
|
||||
);
|
||||
|
||||
// Attach the context per request.
|
||||
baseParams.context = async () => {
|
||||
let req;
|
||||
const contextGenerator = req => {
|
||||
// Pull the user(?) off the request.
|
||||
const { user, jwt } = req;
|
||||
|
||||
try {
|
||||
req = await deserializeUser(upgradeReq);
|
||||
debug(`user ${req.user ? 'was' : 'was not'} on websocket request`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (!user || !jwt) {
|
||||
// There is no valid user on the request, let it continue as is then.
|
||||
return async () => new Context(req);
|
||||
}
|
||||
|
||||
return new Context({});
|
||||
// Provide a flag that can be used to short circuit invalid requests.
|
||||
let expiredLogin = false;
|
||||
|
||||
async function refreshUser() {
|
||||
// Check to see if this request has been short circuited.
|
||||
if (expiredLogin) {
|
||||
// It has, let's exit here.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate that the JWT for this user has not expired.
|
||||
const { exp = false } = jwt;
|
||||
if (exp && exp < Date.now() / 1000) {
|
||||
// Mark that this token has expired, don't bother performing this syscall
|
||||
// again to check the time.
|
||||
expiredLogin = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Let's refresh the user from the database, as they may have changed.
|
||||
const refreshedUser = await batchedUserRefresher.load(user.id);
|
||||
if (!refreshedUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return refreshedUser;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the context builder function that'll use the passed context to
|
||||
// generate future contexts.
|
||||
return async () => {
|
||||
// Refresh the user (potentially null).
|
||||
const refreshedUser = await refreshUser();
|
||||
|
||||
// Attach the refreshedUser to the request.
|
||||
req.user = refreshedUser;
|
||||
|
||||
// Return the new context.
|
||||
return new Context(req);
|
||||
};
|
||||
};
|
||||
|
||||
const onOperation = async (parsedMessage, baseParams, connection) => {
|
||||
// Pull the upgrade request off of the connection.
|
||||
const upgradeReq = connection.upgradeReq;
|
||||
|
||||
// Attach the context handler to the request.
|
||||
baseParams.context = contextGenerator(upgradeReq);
|
||||
|
||||
return baseParams;
|
||||
};
|
||||
|
||||
+227
-6
@@ -1,4 +1,7 @@
|
||||
ar:
|
||||
admin_sidebar:
|
||||
sort_comments: 'فرز التعليقات'
|
||||
view_options: 'عرض الخيارات'
|
||||
already_flagged_username: 'لقد سبق لك وضع علامة باسم المستخدم هذا.'
|
||||
bandialog:
|
||||
are_you_sure: 'أنت متأكد أنك تريد حظر {0}؟'
|
||||
@@ -35,6 +38,9 @@ ar:
|
||||
name: الاسم
|
||||
post: نشر
|
||||
reply: رد
|
||||
comment_history_blank:
|
||||
info: 'سوف يظهر تاريخ تعليقاتك هنا'
|
||||
title: 'لم تكتب أي تعليقات'
|
||||
comment_offensive: 'هذا التعليق مسيء'
|
||||
comment_plural: تعليقات
|
||||
comment_post_banned_word: 'تعليقك يحتوي على كلمة أو أكثر غير مسموح بها، لذا لن يتم نشره. إذا كنت تعتقد أن هذه الرسالة خطأ، رجاء الاتصال بفريق الإشراف لدينا.'
|
||||
@@ -80,6 +86,7 @@ ar:
|
||||
username_and_email: 'اسم المستخدم والبريد الإلكتروني'
|
||||
yes_ban_user: 'نعم إحظر المستخدم'
|
||||
configure:
|
||||
access_message: 'You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!'
|
||||
apply: طبق
|
||||
banned_word_text: 'التعليقات التي تحتوي على هذه الكلمات أو العبارات سيتم حذفها آليا من جدول التعليقات. اطبع كلمة واضغط Enter أو Tab لزيادة الكلمة. اختياريا الصق قائمة مقسمة بالفصلات.'
|
||||
banned_words_title: 'قائمة بالكلمات المحظورة'
|
||||
@@ -104,11 +111,14 @@ ar:
|
||||
custom_css_url_desc: 'رابط CSS الذي سيتجاوز أنماط جدول التعليقات المضمن. يمكن أن يكون داخلي أو خارجي.'
|
||||
days: أيام
|
||||
description: 'كإداري يمكنك تعديل إعدادات جدول التعليقات لهذه القصة:'
|
||||
disable_commenting_desc: 'اكتب رسالة سيتم عرضها أثناء إلغاء تفعيل التعليقات.'
|
||||
disable_commenting_title: 'إلغاء تفعيل التعليقات على مستوى الموقع'
|
||||
domain_list_text: 'أدخل عناوين النطاقات التي سوف تسمح فيها لTalk.. مثلا بيئات التدريج و الإنتاج (مثلا localhost:3000 staging.domain.com domain.com).'
|
||||
domain_list_title: 'النطاقات المسموح بها'
|
||||
edit_comment_timeframe_heading: 'عدل الإطار الزمني للتعليق'
|
||||
edit_comment_timeframe_text_post: 'ثوان لتحرير تعليقاتهم.'
|
||||
edit_comment_timeframe_text_pre: 'سيكون لدى المعلقين'
|
||||
edit_info: 'تحرير المعلومات'
|
||||
embed_comment_stream: 'تضمين الجدول'
|
||||
enable_pre_moderation: 'تمكين الإشراف المسبق'
|
||||
enable_pre_moderation_text: 'يجب على المشرفين الموافقة على أي تعليق قبل نشره.'
|
||||
@@ -129,9 +139,23 @@ ar:
|
||||
open: مفتوح
|
||||
open_stream: 'افتح الجدول'
|
||||
open_stream_configuration: 'جدول التعليقات هذا مفتوح. بإغلاق هذا الجدول لن يتم قبول تعليقات جديدة، وستبقى التعليقات القديمة ظاهرة.'
|
||||
organization_contact_email: 'بريد المنظمة الالكتروني'
|
||||
organization_info_copy: 'نستخدم هذه المعلومات في إشعارات البريد الإلكتروني التي يتم إنشاؤها بواسطة Talk. يعمل هذا على توصيل الرسائل إلى مؤسستك ، ويوفر طريقة للمستخدمين للاتصال بك إذا كانت لديهم مشكلة في حسابهم.'
|
||||
organization_info_copy_2: 'We recommend creating a generic email account (eg. community@yournewsroom.com) for this purpose. This means it can remain consistent over time, and doesn''t expose a name that users could target if their account were blocked.'
|
||||
organization_information: 'معلومات المنظمة'
|
||||
organization_name: 'اسم المنظمة'
|
||||
product_guide_link: 'دليل المنتج'
|
||||
report_bug_or_feedback: 'Report a bug or give feedback'
|
||||
require_email_verification: 'يلزم التحقق من البريد الإلكتروني'
|
||||
require_email_verification_text: 'يجب على المستخدمين الجدد التحقق من بريدهم الإلكتروني قبل التعليق'
|
||||
save: حفظ
|
||||
save_changes: 'احفظ التعديلات'
|
||||
save_changes_dialog:
|
||||
cancel: إلغاء
|
||||
copy: 'لقد أجريت تغييرًا واحدًا أو أكثر بدون حفظ. هل تريد حفظ التغييرات أو تجاهلها؟'
|
||||
discard: تجاهل
|
||||
save_settings: 'احفظ التغييرات'
|
||||
unsaved_changes: 'التغييرات غير المحفوظة'
|
||||
shortcuts: اختصارات
|
||||
sign_out: خروج
|
||||
stories: قصص
|
||||
@@ -140,11 +164,13 @@ ar:
|
||||
suspect_word_title: 'قائمة الكلمات المشبوهة'
|
||||
tech_settings: 'إعدادات تقنية'
|
||||
title: 'تهيئة جدول التعليق'
|
||||
view_last_version: 'عرض أحدث إصدار'
|
||||
weeks: أسابيع
|
||||
wordlist: 'الكلمات المحظورة'
|
||||
confirm_email:
|
||||
click_to_confirm: 'إضغط في الأسفل لتأكيد البريد الألكتروني'
|
||||
confirm: تأكيد
|
||||
email_confirmation: 'تأكيد البريد الإلكتروني'
|
||||
continue: واصل
|
||||
createdisplay:
|
||||
check_the_form: 'استمارة غير صالحة. يرجى التحقق من الحقول'
|
||||
@@ -180,9 +206,13 @@ ar:
|
||||
if_you_did_not: 'إذا لم تطلب ذلك، يمكنك تجاهل هذه الرسالة الإلكترونية.'
|
||||
subject: 'تأكيد البريد الإلكتروني'
|
||||
to_confirm: 'لتأكيد الحساب، يرجى زيارة الرابط التالي:'
|
||||
password_change:
|
||||
body: "تم تغيير كلمة المرور على حسابك. \n\n إذا لم تطلب هذا التغيير ، فيرجى الاتصال بنا على {0}."
|
||||
subject: '{0} تغيير كلمة السر'
|
||||
password_reset:
|
||||
if_you_did: 'اذا فعلت،'
|
||||
please_click: 'الرجاء النقر هنا لإعادة تعيين كلمة المرور'
|
||||
subject: 'إعادة ضبط كلمة المرور'
|
||||
we_received_a_request: 'لقد تلقينا طلبا لإعادة تعيين كلمة المرور. إذا لم تطلب هذا التغيير، فيمكنك تجاهل هذه الرسالة الإلكترونية.'
|
||||
suspended:
|
||||
subject: 'تم تعليق حسابك'
|
||||
@@ -191,28 +221,35 @@ ar:
|
||||
copy: 'نسخ إلى الحافظة'
|
||||
error:
|
||||
ALREADY_EXISTS: 'المورد موجود من قبل'
|
||||
AUTHENTICATION: 'حدث خطأ أثناء محاولة مصادقة حسابك.'
|
||||
CANNOT_IGNORE_STAFF: 'لا يمكن تجاهل الموظفين.'
|
||||
COMMENT_PARENT_NOT_VISIBLE: 'التعليق الذي ترد عليه تمت إزالته أو غير موجود.'
|
||||
COMMENT_TOO_SHORT: 'يجب أن تكون التعليقات أكثر من حرف واحد، يرجى مراجعة تعليقك وإعادة المحاولة.'
|
||||
COMMENTING_CLOSED: 'تم إغلاق فاعلية التعليق'
|
||||
COMMENTING_DISABLED: 'التعليق معطّل حاليًا على هذا الموقع'
|
||||
confirm_password: 'كلمات المرور غير متطابقة. يرجى التحقق مرة أخرى'
|
||||
DELETION_NOT_SCHEDULED: 'لم يكن من المقرر الحذف'
|
||||
EDIT_USERNAME_NOT_AUTHORIZED: 'ليس لديك إذن بتحديث اسم المستخدم الخاص بك.'
|
||||
EDIT_WINDOW_ENDED: 'لم يعد بإمكانك تحرير هذا التعليق. نافذة الوقت للقيام بذلك قد انتهت صلاحيتها.'
|
||||
email: 'ليس بريدا إلكترونيا صالحا'
|
||||
EMAIL_ALREADY_VERIFIED: 'عنوان البريد الإلكتروني تم التحقق منه.'
|
||||
EMAIL_IN_USE: 'البريد الالكتروني قيد الاستخدام'
|
||||
email_not_verified: 'عنوان البريد الإلكتروني {0} لم يتم التحقق منه.'
|
||||
EMAIL_NOT_VERIFIED: 'لم يتم التحقق من عنوان البريد الإلكتروني'
|
||||
email_password: 'مجموعة البريد الإلكتروني و / أو كلمة المرور غير صحيحة.'
|
||||
EMAIL_REQUIRED: 'مطلوب عنوان البريد الإلكتروني'
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: 'رمز التحقق من البريد الإلكتروني غير صالح.'
|
||||
INCORRECT_PASSWORD: 'كلمة سر خاطئة'
|
||||
INVALID_ASSET_URL: 'رابط المادة غير صالح'
|
||||
LOGIN_MAXIMUM_EXCEEDED: 'لقد أجريت العديد من محاولات إدخال كلمة المرور غير الناجحة. أرجو الإنتظار.'
|
||||
network_error: 'فشل الاتصال بالخادم. تحقق من اتصالك بالإنترنت وحاول مرة أخرى.'
|
||||
NO_SPECIAL_CHARACTERS: 'يمكن أن تحتوي أسماء المستخدمين على أحرف, أرقام و _ فقط'
|
||||
NOT_AUTHORIZED: 'غير مصرح لك بتنفيذ هذا الإجراء.'
|
||||
NOT_FOUND: 'المورد غير موجود'
|
||||
organization_contact_email: 'البريد الإلكتروني للمؤسسة غير صالح.'
|
||||
organization_name: 'يجب أن يحتوي اسم المؤسسة على أحرف أو أرقام فقط.'
|
||||
password: 'يجب أن تكون كلمة المرور 8 أحرف على الأقل'
|
||||
PASSWORD_INCORRECT: 'تم إدخال كلمة المرور الحالية بشكل غير صحيح'
|
||||
PASSWORD_LENGTH: 'كلمة المرور قصيرة جدا'
|
||||
PASSWORD_REQUIRED: 'يجب إدخال كلمة مرور'
|
||||
PASSWORD_RESET_TOKEN_INVALID: 'رابط إعادة تعيين كلمة المرور غير صالح.'
|
||||
@@ -227,6 +264,13 @@ ar:
|
||||
USERNAME_REQUIRED: 'يجب إدخال اسم مستخدم'
|
||||
flag_comment: 'الإبلاغ عن تعليق'
|
||||
flag_reason: 'سبب الإبلاغ (اختياري)'
|
||||
flag_reasons:
|
||||
username:
|
||||
impersonating: 'هذا المستخدم ينتحل شخصية'
|
||||
nolike: 'أنا لا أحب اسم المستخدم هذا'
|
||||
offensive: 'اسم المستخدم هذا مسيء'
|
||||
other: آخر
|
||||
spam: 'هذا يشبه الإعلان / التسويق'
|
||||
flag_username: 'بلغ عن اسم المستخدم'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} وافق على اسم المستخدم {1}'
|
||||
@@ -244,6 +288,7 @@ ar:
|
||||
comment_spam: 'غير مرغوب فيه'
|
||||
links: رابط
|
||||
suspect_word: 'كلمة مشتبهة'
|
||||
trust: Karma
|
||||
user:
|
||||
username_impersonating: 'إنتحال شخصية'
|
||||
username_nolike: 'لم يعجبنى'
|
||||
@@ -256,6 +301,7 @@ ar:
|
||||
changed_name:
|
||||
msg: 'يتم مراجعة تغيير اسم المستخدم من قبل فريق الإشراف لدينا.'
|
||||
comment: تعليق
|
||||
comment_is_deleted: 'حذف هذا المعلق حسابه.'
|
||||
comment_is_hidden: 'هذا التعليق غير متاح.'
|
||||
comment_is_ignored: 'هذا التعليق مخفي لأنك تجاهلت هذا المستخدم.'
|
||||
comment_is_rejected: 'لقد رفضت هذا التعليق.'
|
||||
@@ -280,10 +326,92 @@ ar:
|
||||
view_more_comments: 'عرض مزيد من التعليقات'
|
||||
view_reply: 'عرض الرد'
|
||||
from_settings_page: 'من صفحة الملف الشخصي يمكنك مشاهدة سجل التعليقات.'
|
||||
install:
|
||||
add_organization:
|
||||
description: 'Please tell us the name of your organization. This will appear in emails when inviting new team members.'
|
||||
label: 'Organization Name'
|
||||
save: حفظ
|
||||
create:
|
||||
confirm_password: 'تأكيد كلمة المرور'
|
||||
email: 'عنوان البريد الإلكتروني'
|
||||
organization_contact_email: 'Organization Contact Email'
|
||||
password: كلمه المرور
|
||||
save: حفظ
|
||||
username: اسم المستخدم
|
||||
final:
|
||||
close: 'Close this Installer'
|
||||
description: 'Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now.'
|
||||
launch: 'Launch Talk'
|
||||
initial:
|
||||
description: 'Let''s set up your Talk community in just a few short steps.'
|
||||
submit: 'Get Started'
|
||||
permitted_domains:
|
||||
description: 'Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com).'
|
||||
submit: 'Finish install'
|
||||
title: 'Permitted domains'
|
||||
like: إعجاب
|
||||
loading_results: 'جار تحميل النتائج'
|
||||
login:
|
||||
email_address: 'البريد الإلكتروني'
|
||||
forgot_password: 'نسيت كلمه المرور؟'
|
||||
go_back: 'عُد'
|
||||
sign_in: 'تسجيل الدخول'
|
||||
sign_in_button: 'تسجيل الدخول'
|
||||
sign_in_message: 'تسجيل الدخول للتفاعل مع مجتمعك.'
|
||||
password: كلمه المرور
|
||||
request_passowrd: 'اطلب واحدة جديده.'
|
||||
team_sign_in: 'تسجيل الدخول لفريق العمل'
|
||||
marketing: 'هذا يشبه الإعلان / التسويق'
|
||||
moderate_all_streams: 'Moderate comments on All Stories'
|
||||
moderate_this_stream: 'أشرف على هذا الجدول'
|
||||
modqueue:
|
||||
account: 'account flags'
|
||||
actions: Actions
|
||||
all: all
|
||||
all_streams: 'All Streams'
|
||||
approve: Approve
|
||||
approved: Approved
|
||||
ban_user: Ban
|
||||
billion: B
|
||||
close: Close
|
||||
empty_queue: 'No more comments to moderate! You''re all caught up. Go have some ☕️'
|
||||
flagged: flagged
|
||||
jump_to_queue: 'Jump to specific queue'
|
||||
less_detail: 'Less detail'
|
||||
likes: likes
|
||||
million: M
|
||||
mod_faster: 'Moderate faster with keyboard shortcuts'
|
||||
moderate: 'Moderate →'
|
||||
more_detail: 'More detail'
|
||||
navigation: Navigation
|
||||
new: New
|
||||
newest_first: 'Newest First'
|
||||
next_comment: 'Go to the next comment'
|
||||
next_queue: 'Switch queues'
|
||||
notify_accepted: '{0} accepted comment "{1}"'
|
||||
notify_edited: '{0} edited comment "{1}"'
|
||||
notify_flagged: '{0} flagged comment "{1}"'
|
||||
notify_rejected: '{0} rejected comment "{1}"'
|
||||
notify_reset: '{0} reset status of comment "{1}"'
|
||||
oldest_first: 'Oldest First'
|
||||
premod: pre-mod
|
||||
prev_comment: 'Go to the previous comment'
|
||||
reject: Reject
|
||||
rejected: Rejected
|
||||
reply: Reply
|
||||
reported: reported
|
||||
select_stream: 'Select Stream'
|
||||
shift_key: ⇧
|
||||
shortcuts: Shortcuts
|
||||
show_shortcuts: 'Show Shortcuts'
|
||||
singleview: 'Zen mode'
|
||||
sort: Sort
|
||||
system_withheld: 'System Withheld'
|
||||
thismenu: 'Open this menu'
|
||||
thousand: k
|
||||
toggle_search: 'Open search'
|
||||
try_these: 'Try these'
|
||||
view_more_shortcuts: 'View more shortcuts'
|
||||
my_comment_history: 'سجل التعليقات'
|
||||
name: اسم
|
||||
no_agree_comment: 'لا أوافق على هذا التعليق'
|
||||
@@ -292,6 +420,7 @@ ar:
|
||||
other: آخر
|
||||
password_reset:
|
||||
change_password: 'تغيير كلمة السر'
|
||||
change_password_help: 'يرجى إدخال كلمة مرور جديدة لاستخدامها لتسجيل الدخول. اجعلها آمنة!'
|
||||
confirm_new_password: 'تأكيد كلمة السر الجديدة'
|
||||
mail_sent: 'إذا كان لديك حساب مسجل، فقد تم إرسال رابط إعادة تعيين كلمة المرور إلى هذا البريد الإلكتروني'
|
||||
new_password: 'كلمة السر الجديدة'
|
||||
@@ -302,6 +431,25 @@ ar:
|
||||
post: نشر
|
||||
profile: 'الملف الشخصي'
|
||||
profile_settings: إعدادات
|
||||
reject_username:
|
||||
description_notify: 'Suspending this user will temporarily disable their account.'
|
||||
description_reject: 'Would you like to temporarily ban this user because of their {0}? Doing so will temporarily suspend this user until they rewrite their {0}.'
|
||||
email_message_reject: 'Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your username. Please email us if you have any questions or concerns.'
|
||||
no_cancel: 'No cancel'
|
||||
send: Send
|
||||
suspend_user: 'Suspend User'
|
||||
title_notify: 'Notify the user of their temporary suspension'
|
||||
title_reject: 'We noticed you rejected a username'
|
||||
username: username
|
||||
write_message: 'Write a message'
|
||||
yes_suspend: 'Yes suspend'
|
||||
reject_username_dialog:
|
||||
cancel: إلغاء
|
||||
description: 'ساعدنا على الفهم'
|
||||
message: 'سبب الإبلاغ (اختياري)'
|
||||
reason: السبب
|
||||
reject_username: 'رفض اسم المستخدم'
|
||||
title: 'رفض اسم المستخدم'
|
||||
reply: رد
|
||||
report: أبلغ
|
||||
report_notif: 'شكرا على الإبلاغ عن هذا التعليق. تم إبلاغ فريق الإشراف لدينا وسيراجعه قريبًا.'
|
||||
@@ -324,17 +472,90 @@ ar:
|
||||
no_comments: 'لا توجد تعليقات حتى الآن، لماذا لا تكتب واحد؟'
|
||||
no_comments_and_closed: 'لم تكن هناك تعليقات على هذه المقالة.'
|
||||
temporarily_suspended: 'وفقا لإرشادات المجموعة {0}، تم تعليق حسابك مؤقتا. الرجاء إعادة الانضمام إلى المحادثة {1}.'
|
||||
streams:
|
||||
all: All
|
||||
article: Story
|
||||
closed: Closed
|
||||
empty_result: 'No assets match this search. Maybe try widening your search?'
|
||||
filter_streams: 'Filter Streams'
|
||||
most_recent_stories: 'Most Recent Stories'
|
||||
newest: Newest
|
||||
no_results: 'No results'
|
||||
oldest: Oldest
|
||||
open: Open
|
||||
pubdate: 'Publication Date'
|
||||
search: Search
|
||||
search_results: 'Search Results'
|
||||
sort_by: 'Sort By'
|
||||
status: 'Stream Status'
|
||||
stream_status: 'Stream Status'
|
||||
suspenduser:
|
||||
cancel: Cancel
|
||||
day: '{0} days'
|
||||
days: '{0} days'
|
||||
description_notify: 'Suspending this user will temporarily disable their account.'
|
||||
description_suspend: 'You are suspending {0}. This comment will go to the Rejected queue, and {0} will not be allowed to like, report, reply or post until the suspension time is complete.'
|
||||
email_message_suspend: "Dear {0},\n\nIn accordance with {1}’s community guidelines, your account has been temporarily suspended. During the suspension, you will be unable to comment, flag or engage with fellow commenters. Please rejoin the conversation {2}."
|
||||
hour: '{0} hours'
|
||||
hours: '{0} hours'
|
||||
notify_suspend_until: 'User {0} has been temporarily suspended. This suspension will automatically end {1}.'
|
||||
one_hour: '1 hour'
|
||||
select_duration: 'Select suspension duration'
|
||||
send: Send
|
||||
suspend_user: 'Suspend User'
|
||||
title_notify: 'Notify the user of their temporary suspension'
|
||||
title_suspend: 'Suspend User'
|
||||
write_message: 'Write a message'
|
||||
thank_you: 'نحن نقدر سلامتك وردود الفعل. سيراجع المشرف التقرير الخاص بك'
|
||||
user:
|
||||
bio_flags: 'flags for this bio'
|
||||
user_bio: 'User Bio'
|
||||
username_flags: 'flags for this username'
|
||||
user_detail:
|
||||
all: All
|
||||
ban: 'Ban User'
|
||||
banned: Banned
|
||||
email: Email
|
||||
id: ID
|
||||
karma: Karma
|
||||
karma_docs_link: 'https://docs.coralproject.net/talk/trust/#user-karma-score'
|
||||
learn_more: 'Learn More'
|
||||
member_since: 'Member Since'
|
||||
reject_rate: 'Reject Rate'
|
||||
reject_username: 'Reject Username'
|
||||
rejected: Rejected
|
||||
remove_ban: 'Remove Ban'
|
||||
remove_suspension: 'Remove Suspension'
|
||||
suspend: 'Suspend User'
|
||||
suspended: Suspended
|
||||
total_comments: 'Total Comments'
|
||||
unreliable: Unreliable
|
||||
user_history: 'User History'
|
||||
user_karma_score: 'User Karma Score'
|
||||
username: Username
|
||||
username_needs_approval: 'Username needs approval'
|
||||
username_rejected: 'Username rejected'
|
||||
user_history:
|
||||
action: Action
|
||||
ban_removed: 'Ban removed'
|
||||
date: Date
|
||||
suspended: 'Suspended, {0}'
|
||||
suspension_removed: 'Suspension removed'
|
||||
system: System
|
||||
taken_by: 'Taken By'
|
||||
user_banned: 'User banned'
|
||||
username_status: 'Username {0}'
|
||||
user_impersonating: 'هذا المستخدم ينتحل شخصية'
|
||||
user_no_comment: 'لم تترك تعليقا مطلقا. إنضم إلى المحادثة!'
|
||||
username_offensive: 'اسم المستخدم هذا مسيء'
|
||||
validators:
|
||||
confirm_password: 'ﻚﻠﻣﺎﺗ ﺎﻠﻣﺭﻭﺭ ﻎﻳﺭ ﻢﺘﻃﺎﺒﻗﺓ. ﻱﺮﺟﻯ ﺎﻠﺘﺤﻘﻗ ﻡﺭﺓ ﺄﺧﺭﻯ'
|
||||
required: 'ﻩﺬﻫ ﺎﻠﺧﺎﻧﺓ ﻢﻄﻟﻮﺒﻫ'
|
||||
verify_email: 'ﻞﻴﺳ ﺏﺮﻳﺩﺍ ﺈﻠﻜﺗﺭﻮﻨﻳﺍ ﺹﺎﻠﺣﺍ'
|
||||
verify_organization_name: 'ﻲﺠﺑ ﺄﻧ ﻲﺤﺗﻮﻳ ﺎﺴﻣ ﺎﻠﻣﺆﺴﺳﺓ ﻊﻟﻯ ﺄﺣﺮﻓ ﺃﻭ ﺃﺮﻗﺎﻣ ﻒﻘﻃ.'
|
||||
verify_password: 'ﻲﺠﺑ ﺄﻧ ﺖﻛﻮﻧ ﻚﻠﻣﺓ ﺎﻠﻣﺭﻭﺭ 8 ﺄﺣﺮﻓ ﻊﻟﻯ ﺍﻸﻘﻟ'
|
||||
verify_username: 'ﻲﻤﻜﻧ ﺄﻧ ﺖﺤﺗﻮﻳ ﺄﺴﻣﺍﺀ ﺎﻠﻤﺴﺘﺧﺪﻤﻴﻧ ﻊﻟﻯ ﺃﺮﻗﺎﻣ, ﺄﺣﺮﻓ ﻭ _ ﻒﻘﻃ'
|
||||
confirm_email: 'عناوين البريد الإلكتروني لا تتطابق. يرجى التحقق مرة أخرى.'
|
||||
confirm_password: 'كلمات المرور غير متطابقة. يرجى التحقق مرة أخرى'
|
||||
required: 'هذه الخانة مطلوبة'
|
||||
verify_email: 'ليس بريدا إلكترونيا صالحا'
|
||||
verify_organization_name: 'يجب أن يحتوي اسم المؤسسة على أحرف أو أرقام فقط.'
|
||||
verify_password: 'يجب أن تكون كامة المرور 8 أحرف على اﻷقل'
|
||||
verify_username: 'يمكن أن تحتوي أسماء المستخدمين على أرقام, أحرف ﻭ _ فقط'
|
||||
view_conversation: 'عرض المحادثة'
|
||||
your_account_has_been_banned: 'تم حظر حسابك.'
|
||||
your_account_has_been_suspended: 'تم تعليق حسابك مؤقتا.'
|
||||
|
||||
@@ -163,6 +163,12 @@ Comment.index({
|
||||
created_at: -1,
|
||||
});
|
||||
|
||||
Comment.index({
|
||||
asset_id: 1,
|
||||
parent_id: 1,
|
||||
created_at: 1,
|
||||
});
|
||||
|
||||
Comment.index(
|
||||
{
|
||||
'action_counts.flag': 1,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "4.5.1",
|
||||
"version": "4.6.2",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
|
||||
@@ -25,85 +25,104 @@ let enabled = true;
|
||||
// }
|
||||
// });
|
||||
|
||||
async function checkForSpam(ctx, { asset_id, body }) {
|
||||
const req = ctx.parent.parent;
|
||||
const loaders = ctx.loaders;
|
||||
|
||||
//If the key validation failed, then we can't run with the client.
|
||||
if (!enabled) {
|
||||
debug('not enabled, passing');
|
||||
return;
|
||||
}
|
||||
|
||||
let spam = false;
|
||||
try {
|
||||
const user_ip = get(req, 'ip', false);
|
||||
if (!user_ip) {
|
||||
debug('no ip on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get some headers from the request.
|
||||
const user_agent = req.get('User-Agent');
|
||||
if (!user_agent || user_agent.length === 0) {
|
||||
debug('no user agent on request');
|
||||
return;
|
||||
}
|
||||
|
||||
const referrer = req.get('Referrer');
|
||||
if (!referrer || referrer.length === 0) {
|
||||
debug('no referrer on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the Asset that the comment is being made against.
|
||||
const asset = await loaders.Assets.getByID.load(asset_id);
|
||||
if (!asset) {
|
||||
debug('asset not found for new comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send off the comment to Akismet to check to see what they say.
|
||||
spam = await client.checkSpam({
|
||||
user_ip,
|
||||
user_agent,
|
||||
referrer,
|
||||
permalink: asset.url,
|
||||
comment_type: 'comment',
|
||||
comment_content: body,
|
||||
is_test: false,
|
||||
});
|
||||
|
||||
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
|
||||
|
||||
return spam;
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePositiveSpam(input) {
|
||||
// Attach reason information for the flag being added.
|
||||
input.status = 'SYSTEM_WITHHELD';
|
||||
input.actions =
|
||||
input.actions && input.actions.length >= 0 ? input.actions : [];
|
||||
input.actions.push({
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'SPAM_COMMENT',
|
||||
metadata: {},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RootMutation: {
|
||||
createComment: {
|
||||
async pre(_, { input }, ctx) {
|
||||
const req = ctx.parent.parent;
|
||||
const loaders = ctx.loaders;
|
||||
|
||||
//If the key validation failed, then we can't run with the client.
|
||||
if (!enabled) {
|
||||
debug('not enabled, passing');
|
||||
return;
|
||||
editComment: {
|
||||
pre: async (_, { asset_id, edit: { body }, edit }, ctx) => {
|
||||
const spam = await checkForSpam(ctx, { asset_id, body });
|
||||
if (spam) {
|
||||
// Mark the comment as positive spam.
|
||||
handlePositiveSpam(edit);
|
||||
}
|
||||
|
||||
let spam = false;
|
||||
try {
|
||||
const user_ip = get(req, 'ip', false);
|
||||
if (!user_ip) {
|
||||
debug('no ip on request');
|
||||
return;
|
||||
},
|
||||
},
|
||||
createComment: {
|
||||
pre: async (_, { input }, ctx) => {
|
||||
const spam = await checkForSpam(ctx, input);
|
||||
if (spam) {
|
||||
if (input.checkSpam) {
|
||||
throw new ErrSpam();
|
||||
}
|
||||
|
||||
// Get some headers from the request.
|
||||
const user_agent = req.get('User-Agent');
|
||||
if (!user_agent || user_agent.length === 0) {
|
||||
debug('no user agent on request');
|
||||
return;
|
||||
}
|
||||
|
||||
const referrer = req.get('Referrer');
|
||||
if (!referrer || referrer.length === 0) {
|
||||
debug('no referrer on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the Asset that the comment is being made against.
|
||||
const asset = await loaders.Assets.getByID.load(input.asset_id);
|
||||
if (!asset) {
|
||||
debug('asset not found for new comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send off the comment to Akismet to check to see what they say.
|
||||
spam = await client.checkSpam({
|
||||
user_ip,
|
||||
user_agent,
|
||||
referrer,
|
||||
permalink: asset.url,
|
||||
comment_type: 'comment',
|
||||
comment_content: input.body,
|
||||
is_test: false,
|
||||
});
|
||||
|
||||
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
return;
|
||||
// Mark the comment as positive spam.
|
||||
handlePositiveSpam(input);
|
||||
}
|
||||
|
||||
// Attach scores to metadata.
|
||||
input.metadata = merge({}, input.metadata || {}, {
|
||||
akismet: spam,
|
||||
});
|
||||
|
||||
if (spam) {
|
||||
if (input.checkSpam) {
|
||||
throw new ErrSpam();
|
||||
}
|
||||
|
||||
// Attach reason information for the flag being added.
|
||||
input.status = 'SYSTEM_WITHHELD';
|
||||
input.actions =
|
||||
input.actions && input.actions.length >= 0 ? input.actions : [];
|
||||
input.actions.push({
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'SPAM_COMMENT',
|
||||
metadata: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ ar:
|
||||
or: "أو"
|
||||
email: "البريد الإلكتروني"
|
||||
password: "كلمة المرور"
|
||||
password_error: "يجب أن تكون كلمة المرور 8 أحرف على الأقل."
|
||||
forgot_your_pass: "نسيت كلمة المرور؟"
|
||||
need_an_account: "تحتاج الى حساب؟"
|
||||
register: "تسجيل"
|
||||
@@ -43,6 +44,15 @@ ar:
|
||||
username: اسم المستخدم
|
||||
write_your_username: "عدل اسم المستخدم"
|
||||
your_username: "يظهر اسم المستخدم في كل تعليق تنشره."
|
||||
change_password:
|
||||
change_password: "تغيير كلمة المرور"
|
||||
passwords_dont_match: "كلمات المرور غير متطابقة"
|
||||
required_field: "هذه الخانة مطلوبه"
|
||||
forgot_password: "نسيت كلمة المرور؟"
|
||||
save: "حفظ"
|
||||
cancel: "إلغاء"
|
||||
edit: "تصحيح"
|
||||
changed_password_msg: "كلمة المرور الخاصة بك تم تغييرها بنجاح"
|
||||
da:
|
||||
talk-plugin-auth:
|
||||
login:
|
||||
|
||||
@@ -27,7 +27,7 @@ module.exports = passport => {
|
||||
try {
|
||||
const { id, provider, displayName } = profile;
|
||||
|
||||
user = await UsersService.findOrCreateExternalUser(
|
||||
user = await UsersService.upsertSocialUser(
|
||||
req.context,
|
||||
id,
|
||||
provider,
|
||||
|
||||
@@ -26,7 +26,7 @@ module.exports = passport => {
|
||||
try {
|
||||
const { id, provider, displayName } = profile;
|
||||
|
||||
user = await UsersService.findOrCreateExternalUser(
|
||||
user = await UsersService.upsertSocialUser(
|
||||
req.context,
|
||||
id,
|
||||
provider,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
ar:
|
||||
talk-plugin-ignore-user:
|
||||
blank_info: أنت لا تتجاهل حاليًا أي مستخدم
|
||||
section_title: المستخدمون الذين تم تجاهلهم
|
||||
section_info: لأنك تجاهلت المعلقين التاليين، يتم إخفاء تعليقاتهم.
|
||||
stop_ignoring: إيقاف التجاهل
|
||||
|
||||
@@ -1,3 +1,79 @@
|
||||
ar:
|
||||
email:
|
||||
email_change_original:
|
||||
subject: تغيير البريد الإلكتروني
|
||||
body: تم تغيير عنوان بريدك الإلكتروني من {0} إلى {1}. إذا لم تطلب هذا التغيير ، فيرجى الاتصال {2}.
|
||||
error:
|
||||
NO_LOCAL_PROFILE: لا يوجد عنوان بريد إلكتروني حالي مقترن بهذا الحساب.
|
||||
LOCAL_PROFILE: هناك بريد إلكتروني مرتبط بهذا الحساب.
|
||||
INCORRECT_PASSWORD: كلمة المرور المقدمة غير صحيحة.
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
change_password: "تغيير كلمة المرور"
|
||||
passwords_dont_match: "كلمات المرور لا تتطابق"
|
||||
required_field: "هذه الخانة مطلوبه"
|
||||
forgot_password: "نسيت كلمة المرور؟"
|
||||
old_password: "كلمة المرور القديمة"
|
||||
new_password: "كلمة المرور الجديدة"
|
||||
confirm_new_password: "تأكيد كلمة المرور الجديدة"
|
||||
save: "حفظ"
|
||||
cancel: "إلغاء"
|
||||
edit: "تعديل"
|
||||
changed_password_msg: "تغيير كلمة المرور - تم تغيير كلمة المرور الخاصة بك بنجاح"
|
||||
forgot_password_sent: "نسيت كلمة المرور - لقد أرسلنا إليك رسالة بريد إلكتروني لاسترداد كلمة المرور الخاصة بك"
|
||||
change_username:
|
||||
change_username_note: "لا يمكن تغيير أسماء المستخدمين إلا مرة واحدة كل 14 يومًا."
|
||||
is_not_eligible: "لا يمكنك حاليا تغيير اسم المستخدم الخاص بك."
|
||||
save: "حفظ"
|
||||
edit_profile: "تعديل الملف الشخصي"
|
||||
cancel: "إلغاء"
|
||||
confirm_username_change: "تأكيد تغيير اسم المستخدم"
|
||||
description: "أنت تحاول تغيير اسم المستخدم الخاص بك. سوف يظهر اسم المستخدم الجديد الخاص بك على جميع تعليقاتك الماضية والتعليقات المستقبلية."
|
||||
old_username: "اسم المستخدم القديم"
|
||||
new_username: "اسم المستخدم الجديد"
|
||||
re_enter: "أعد إدخال اسم مستخدم جديد"
|
||||
bottom_note: "ملاحظة: لن تتمكن من تغيير اسم المستخدم الخاص بك مرة أخرى لمدة 14 يومًا"
|
||||
confirm_changes: "تأكيد التغييرات"
|
||||
username_does_not_match: "اسم المستخدم غير متطابق"
|
||||
cant_be_equal: "يجب أن يكون {0} الجديد الخاص بك مختلفًا عن الحالي"
|
||||
changed_username_success_msg: "اسم المستخدم تم تغييره - تم تغيير اسم المستخدم الخاص بك بنجاح. لن تتمكن من تغيير اسم المستخدم الخاص بك لمدة 14 يومًا."
|
||||
change_username_attempt: "لا يمكن تحديث اسم المستخدم. لا يمكن تغيير أسماء المستخدمين إلا كل 14 يومًا."
|
||||
change_email:
|
||||
confirm_email_change: "تأكيد تغيير عنوان البريد الإلكتروني"
|
||||
description: "أنت تحاول تغيير عنوان بريدك الإلكتروني. سيتم استخدام عنوان بريدك الإلكتروني الجديد لتسجيل الدخول ولتلقي إشعارات الحساب."
|
||||
old_email: "عنوان البريد الإلكتروني القديم"
|
||||
new_email: "عنوان البريد الإلكتروني الجديد"
|
||||
enter_password: "أدخل كلمة المرور"
|
||||
incorrect_password: "كلمة مرور خاطئة"
|
||||
confirm_change: "تأكيد التغيير"
|
||||
cancel: "إلغاء"
|
||||
change_email_msg: "تم تغيير عنوان البريد الإلكتروني. سيتم استخدام عنوان البريد الإلكتروني هذا الآن لتسجيل الدخول ولإشعارات البريد الإلكتروني."
|
||||
add_email:
|
||||
add_email_address: "إضافة البريد الإلكتروني"
|
||||
enter_email_address: "أدخل البريد الالكتروني:"
|
||||
invalid_email_address: "البريد الإلكتروني غير صالح"
|
||||
confirm_email_address: "أكد عنوان بريدك الإلكتروني:"
|
||||
email_does_not_match: "عنوان البريد الإلكتروني غير مطابق"
|
||||
insert_password: "إدخال كلمة المرور:"
|
||||
confirm_password: "تأكيد كلمة المرور:"
|
||||
required_field: "هذه الخانة مطلوبه"
|
||||
done: "تم"
|
||||
content:
|
||||
title: "أضف عنوان بريد إلكتروني"
|
||||
description: "لمزيد من الأمان ، نطلب من المستخدمين إضافة عنوان بريد إلكتروني إلى حساباتهم. سيتم استخدام عنوان بريدك الإلكتروني في:"
|
||||
item_1: "تلقي التحديثات المتعلقة بأي تغييرات في حسابك (عنوان البريد الإلكتروني ، اسم المستخدم ، كلمة المرور ، إلخ.)"
|
||||
item_2: "السماح لك بتنزيل تعليقاتك."
|
||||
item_3: "إرسال إشعارات التعليقات التي اخترت استلامها."
|
||||
verify:
|
||||
title: "تحقق من عنوان البريد الإلكتروني الخاص بك"
|
||||
description: "لقد أرسلنا رسالة إلكترونية إلى {0} لإثبات ملكية حسابك. يجب عليك التحقق من عنوان بريدك الإلكتروني حتى يمكن استخدامه لتأكيد تعديلات الحساب والإشعارات."
|
||||
added:
|
||||
title: "تمت إضافة عنوان البريد الالكتروني"
|
||||
description: "تمت إضافة عنوان بريدك الإلكتروني إلى حسابك."
|
||||
subtitle: "هل تحتاج إلى تغيير عنوان بريدك الإلكتروني؟"
|
||||
description_2: "يمكنك تغيير إعدادات حسابك من خلال زيارة"
|
||||
path: "ملفي > الإعدادات"
|
||||
alert: "تمت إضافة البريد الإلكتروني!"
|
||||
en:
|
||||
email:
|
||||
email_change_original:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
ar:
|
||||
talk-plugin-notifications-category-featured:
|
||||
toggle_description: تعليقي تم تمييزه
|
||||
en:
|
||||
talk-plugin-notifications-category-featured:
|
||||
toggle_description: My comment is featured
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
ar:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
featured:
|
||||
subject: "تميَزت واحدة من تعليقاتك على {0}"
|
||||
body: "{0}\n
|
||||
حدد أحد أعضاء فريقنا هذا التعليق ليتم عرضه كتعليق مميز للقراء الآخرين: {1}"
|
||||
en:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
ar:
|
||||
talk-plugin-notifications-category-reply:
|
||||
toggle_description: يتلقى تعليقي ردا
|
||||
en:
|
||||
talk-plugin-notifications-category-reply:
|
||||
toggle_description: My comment receives a reply
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
ar:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
reply:
|
||||
subject: "رد شخص ما على تعليقك على {0}"
|
||||
body: "{0}\n{1} رد على تعليقك
|
||||
{2}"
|
||||
en:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
ar:
|
||||
talk-plugin-notifications-category-staff:
|
||||
toggle_description: يرد أحد الموظفين على تعليقي
|
||||
en:
|
||||
talk-plugin-notifications-category-staff:
|
||||
toggle_description: A staff member replies to my comment
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
ar:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
staff:
|
||||
subject: "شخص ما في {0} قد رد على تعليقك"
|
||||
body: "{0}\n{1} يعمل ل
|
||||
{2} وقد رد على تعليقك: {3}"
|
||||
en:
|
||||
talk-plugin-notifications:
|
||||
categories:
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
ar:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
DAILY: في ملخص يومي
|
||||
en:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
ar:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
HOURLY: في ملخص كل ساعة
|
||||
en:
|
||||
talk-plugin-notifications:
|
||||
digest_enum:
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
ar:
|
||||
talk-plugin-notifications:
|
||||
settings_title: إشعارات
|
||||
settings_subtitle: تلقي الإشعارات متى
|
||||
turn_off_all: لا أريد تلقي الإشعارات
|
||||
banner_info:
|
||||
title: التحقق من البريد الإلكتروني مطلوب
|
||||
text: لتلقي إشعارات البريد الإلكتروني ، يجب أن يكون لديك عنوان بريد إلكتروني تم التحقق منه.
|
||||
verify_now: تحقق من بريدك الالكتروني الآن
|
||||
banner_success:
|
||||
title: تم إرسال التحقق عبر البريد الإلكتروني
|
||||
text: تم إرسال رسالة إلكترونية إلى {0} يحتوي على رابط التحقق.
|
||||
banner_error:
|
||||
title: خطأ
|
||||
text: حدث خطأ في إرسال رسالة التحقق الخاصة بك. الرجاء معاودة المحاولة في وقت لاحق.
|
||||
digest_option: إرسال الإشعارات
|
||||
digest_enum:
|
||||
NONE: فورا
|
||||
en:
|
||||
talk-plugin-notifications:
|
||||
settings_title: Notifications
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
ar:
|
||||
talk-plugin-notifications:
|
||||
templates:
|
||||
digest:
|
||||
subject: "نشاطك الأخير للتعليق على {0}"
|
||||
footer: "لقد تلقيت هذا الإشعار نظرًا لأنك معلق على {0} وتم تمكين تلقي الإشعارات."
|
||||
links:
|
||||
unsubscribe: "إلغاء الاشتراك في إشعارات التعليقات"
|
||||
unsubscribe_page:
|
||||
unsubscribe: "إلغاء الاشتراك في إشعارات التعليقات"
|
||||
click_to_confirm: "انقر أدناه لتأكيد رغبتك في إلغاء الاشتراك من جميع الإشعارات"
|
||||
confirm: "أكد"
|
||||
are_unsubscribed: "أنت الآن غير مشترك في جميع الإشعارات."
|
||||
token_invalid: "رابط إلغاء الاشتراك غير صالح ، انقر الرابط من بريد إلكتروني أحدث أو قم بزيارة جدول تعليقات وتسجيل الدخول لتغيير تفضيلات الإشعارات الخاصة بك"
|
||||
en:
|
||||
talk-plugin-notifications:
|
||||
templates:
|
||||
|
||||
@@ -3,6 +3,8 @@ import { gql } from 'react-apollo';
|
||||
import moment from 'moment';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
import { scheduledDeletionDelayHours } from '../../config';
|
||||
|
||||
export const withRequestDownloadLink = withMutation(
|
||||
gql`
|
||||
mutation DownloadCommentHistory {
|
||||
@@ -48,7 +50,7 @@ export const withRequestAccountDeletion = withMutation(
|
||||
});
|
||||
|
||||
const scheduledDeletionDate = moment()
|
||||
.add(24, 'hours')
|
||||
.add(scheduledDeletionDelayHours, 'hours')
|
||||
.toDate();
|
||||
|
||||
const data = update(prev, {
|
||||
|
||||
@@ -1,3 +1,54 @@
|
||||
ar:
|
||||
download_request:
|
||||
section_title: "قم بتنزيل سجل التعليقات الخاص بي"
|
||||
you_will_get_a_copy: "ستتلقى رسالة إلكترونية تحتوي على رابط لتنزيل سجل التعليقات. تستطيع طلب"
|
||||
download_rate: "طلب تنزيل واحد كل {0} يوم"
|
||||
most_recent_request: "طلبك الأخير"
|
||||
request: "طلب سجل التعليقات"
|
||||
rate_limit: "يمكنك تقديم طلب سجل تعليقات آخر في {0}"
|
||||
hours: "{0} ساعات"
|
||||
days: "{0} أيام"
|
||||
hour: "{0} ساعة"
|
||||
day: "{0} يوم"
|
||||
download_preparing: "جارٍ التحضير لتنزيل الحساب - تحقق من بريدك الإلكتروني قريبًا للحصول على رابط تنزيل"
|
||||
delete_request:
|
||||
account_deletion_cancelled: 'طلب حذف الحساب تم إلغاؤه - لقد تم إلغاء طلبك لحذف حسابك.'
|
||||
account_deletion_requested: 'تم طلب حذف الحساب'
|
||||
received_on: "تم تلقي طلب لحذف حسابك في "
|
||||
cancel_request_description: "إذا كنت ترغب في إعادة تفعيل حسابك ، فيمكنك إلغاء طلبك لحذف حسابك أدناه"
|
||||
before: "قبل"
|
||||
cancel_account_deletion_request: "إلغاء طلب حذف الحساب"
|
||||
delete_my_account: "احذف حسابي"
|
||||
delete_my_account_description: "سيؤدي حذف حسابك إلى محو ملفك الشخصي نهائيًا وإزالة جميع تعليقاتك من هذا الموقع."
|
||||
already_submitted_request_description: "لقد أرسلت طلبًا لحذف حسابك. سيتم حذف حسابك بعد {0}. يمكنك إلغاء الطلب حتى ذلك الوقت"
|
||||
your_request_submitted_description: "تم إرسال طلبك وتم إرسال التأكيد إلى عنوان البريد الإلكتروني المرتبط بحسابك."
|
||||
your_account_deletion_scheduled: "من المقرر أن يتم حذف حسابك بعد:"
|
||||
changed_your_mind: "غيرت رأيك؟"
|
||||
simply_go_to: "ما عليك سوى الانتقال إلى حسابك مرة أخرى قبل هذا الوقت والنقر"
|
||||
tell_us_why: "أخبرنا لماذا"
|
||||
feedback_copy: "نود أن نعرف لماذا اخترت حذف حسابك. أرسل لنا ردود الفعل على نظام التعليق لدينا عن طريق البريد الإلكتروني"
|
||||
done: "تم"
|
||||
cancel: "إلغاء"
|
||||
proceed: "تقدم"
|
||||
input_is_not_correct: "المدخلات غير صحيحة"
|
||||
step_0:
|
||||
you_are_attempting: "أنت تحاول حذف حسابك. هذا يعنى:"
|
||||
item_1: "تم إزالة جميع تعليقاتك من هذا الموقع"
|
||||
item_2: "تم حذف جميع تعليقاتك من قاعدة البيانات الخاصة بنا"
|
||||
item_3: "تم إزالة اسم المستخدم وعنوان البريد الإلكتروني من نظامنا"
|
||||
step_1:
|
||||
subtitle: "متى سيتم حذف حسابي؟"
|
||||
description: "سيتم حذف حسابك بعد {0} ساعات من تقديم طلبك."
|
||||
subtitle_2: "هل ما زال بإمكاني كتابة تعليقات حتى يتم حذف حسابي؟"
|
||||
description_2: "نعم ، لا يزال بإمكانك التعليق والرد على التعليقات والتفاعل عليها حتى تنتهي صلاحية {0} ساعات."
|
||||
step_2:
|
||||
description: "قبل حذف حسابك، نوصيك بتنزيل سجل التعليقات الخاص بسجلاتك. بعد حذف حسابك، لن تتمكن من طلب سجل التعليقات الخاص بك."
|
||||
to_download: "لتنزيل سجل التعليقات، انتقل إلى:"
|
||||
path: "ملفي > تنزيل سجل تعليقاتي"
|
||||
step_3:
|
||||
subtitle: "هل انت متأكد انك تريد حذف حسابك؟"
|
||||
description: "للتأكيد على رغبتك في حذف حسابك، يرجى كتابة العبارة التالية في مربع النص أدناه:"
|
||||
type_to_confirm: "اكتب عبارة أدناه للتأكيد"
|
||||
en:
|
||||
download_request:
|
||||
section_title: "Download My Comment History"
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
ar:
|
||||
download_landing:
|
||||
download_your_account: "قم بتنزيل سجل التعليقات الخاص بك"
|
||||
download_details: "سيتم تنزيل سجل التعليقات في ملف .zip. بعد فك ضغط محفوظات التعليقات ، سيكون لديك ملف ذو قيمة مفصولة بفاصلة (أو .csv) يمكنك استيراده بسهولة إلى تطبيق جدول البيانات المفضل لديك."
|
||||
all_information_included: "لكل من تعليقاتك يتم تضمين المعلومات التالية:"
|
||||
information_included:
|
||||
date: "متى كتبت التعليق"
|
||||
url: "الرابط الثابت للتعليق"
|
||||
body: "نص التعليق"
|
||||
asset_url: "عنوان URL للمقال أو القصة التي يظهر فيها التعليق"
|
||||
confirm: "تنزيل محفوظات تعليقاتي"
|
||||
email:
|
||||
download:
|
||||
subject: "تعليقاتك جاهزة للتنزيل من {0}"
|
||||
download_link_ready: "انقر هنا لتنزيل تعليقاتك من
|
||||
{0} اعتبارا من {1}:"
|
||||
download_archive: "تنزيل الأرشيف"
|
||||
delete:
|
||||
subject: "حسابك ل {0} من المقرر أن يتم حذفه"
|
||||
body: |
|
||||
تم تلقي طلب لحذف حسابك. تمت جدولة حسابك للحذف في {1}.
|
||||
|
||||
بعد ذلك الوقت ، ستتم إزالة جميع تعليقاتك من الموقع ، وستتم إزالة جميع تعليقاتك من قاعدة بياناتنا ، وستتم إزالة اسم المستخدم وعنوان البريد الإلكتروني من نظامنا.
|
||||
|
||||
إذا غيرت رأيك ، يمكنك تسجيل الدخول إلى حسابك وإلغاء الطلب قبل وقت حذف الحساب المجدول.
|
||||
deleted:
|
||||
subject: "حسابك ل {0} قد تم حذفه"
|
||||
body: |
|
||||
حساب المعلق الخاص بك ل {0} تم حذفه الآن. نحن آسفون أن نراك تذهب!
|
||||
|
||||
إذا كنت ترغب في إعادة الانضمام إلى المناقشة في المستقبل ، يمكنك الاشتراك للحصول على حساب جديد.
|
||||
|
||||
إذا كنت ترغب في تزويدنا بتعليقات حول سبب تركك وما يمكننا فعله لجعل تجربة التعليق أفضل ، يرجى مراسلتنا عبر البريد الإلكتروني على {1}.
|
||||
cancelDelete:
|
||||
subject: "طلب حذف حسابك ل {0} تم إلغاؤه"
|
||||
body: "لقد ألغيت طلب حذف حسابك ل {0}. تم الآن إعادة تفعيل حسابك."
|
||||
error:
|
||||
DOWNLOAD_TOKEN_INVALID: "رابط التنزيل الخاص بك غير صالح."
|
||||
en:
|
||||
download_landing:
|
||||
download_your_account: "Download Your Comment History"
|
||||
|
||||
@@ -1,40 +1,59 @@
|
||||
const { getScores, isToxic } = require('./perspective');
|
||||
const { ErrToxic } = require('./errors');
|
||||
|
||||
function handlePositiveToxic(input) {
|
||||
input.status = 'SYSTEM_WITHHELD';
|
||||
input.actions =
|
||||
input.actions && input.actions.length >= 0 ? input.actions : [];
|
||||
input.actions.push({
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'TOXIC_COMMENT',
|
||||
metadata: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function getScore(body) {
|
||||
// Try getting scores.
|
||||
let scores;
|
||||
try {
|
||||
scores = await getScores(body);
|
||||
} catch (err) {
|
||||
// Warn and let mutation pass.
|
||||
console.trace(err); // TODO: log/handle this differently?
|
||||
return;
|
||||
}
|
||||
|
||||
return scores;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RootMutation: {
|
||||
editComment: {
|
||||
pre: async (_, { edit: { body }, edit }) => {
|
||||
const scores = await getScore(body);
|
||||
if (isToxic(scores)) {
|
||||
handlePositiveToxic(edit);
|
||||
}
|
||||
},
|
||||
},
|
||||
createComment: {
|
||||
async pre(_, { input }, _context, _info) {
|
||||
// Try getting scores.
|
||||
let scores;
|
||||
try {
|
||||
scores = await getScores(input.body);
|
||||
} catch (err) {
|
||||
// Warn and let mutation pass.
|
||||
console.trace(err); // TODO: log/handle this differently?
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach scores to metadata.
|
||||
input.metadata = Object.assign({}, input.metadata, {
|
||||
perspective: scores,
|
||||
});
|
||||
const scores = await getScore(input.body);
|
||||
|
||||
if (isToxic(scores)) {
|
||||
if (input.checkToxicity) {
|
||||
throw new ErrToxic();
|
||||
}
|
||||
|
||||
input.status = 'SYSTEM_WITHHELD';
|
||||
input.actions =
|
||||
input.actions && input.actions.length >= 0 ? input.actions : [];
|
||||
input.actions.push({
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'TOXIC_COMMENT',
|
||||
metadata: {},
|
||||
});
|
||||
// Mark the comment as positive toxic.
|
||||
handlePositiveToxic(input);
|
||||
}
|
||||
|
||||
// Attach scores to metadata.
|
||||
input.metadata = Object.assign({}, input.metadata, {
|
||||
perspective: scores,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+84
-40
@@ -48,6 +48,60 @@ const loginRateLimiter = new Limit(
|
||||
RECAPTCHA_WINDOW
|
||||
);
|
||||
|
||||
// upsertUser will try to lookup the user by their profile. If the user cannot
|
||||
// be looked up, it will create one with a unique username and the designated
|
||||
// username status.
|
||||
async function upsertUser(
|
||||
ctx,
|
||||
id,
|
||||
provider,
|
||||
displayName,
|
||||
usernameStatus,
|
||||
shouldSetDisplayName = false
|
||||
) {
|
||||
let user = await User.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id,
|
||||
provider,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
// User does not exist and need to be created.
|
||||
|
||||
// Create an initial username for the user.
|
||||
let username = await Users.getInitialUsername(displayName);
|
||||
|
||||
// The user was not found, lets create them!
|
||||
user = new User({
|
||||
username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
profiles: [{ id, provider }],
|
||||
status: {
|
||||
username: {
|
||||
status: usernameStatus,
|
||||
history: [{ status: usernameStatus }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (shouldSetDisplayName) {
|
||||
// Set the displayName on the user metadata so that it can be accessed.
|
||||
user.metadata = user.metadata || {};
|
||||
user.metadata.displayName = displayName;
|
||||
}
|
||||
|
||||
// Save the user in the database.
|
||||
await user.save();
|
||||
|
||||
// Emit that the user was created.
|
||||
ctx.pubsub.publish('userCreated', user);
|
||||
}
|
||||
|
||||
// Users is the interface for the application to interact with the
|
||||
// User through.
|
||||
class Users {
|
||||
@@ -478,52 +532,42 @@ class Users {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* upsertExternalUser will create or lookup a user where the user will not be
|
||||
* able to change their username.
|
||||
*
|
||||
* @param {Object} ctx the graph context
|
||||
* @param {String} id the ID for the user from the provider
|
||||
* @param {String} provider the name of the provider
|
||||
* @param {String} displayName the users desired displayName, not guaranteed
|
||||
*/
|
||||
static async upsertExternalUser(ctx, id, provider, displayName) {
|
||||
return upsertUser(ctx, id, provider, displayName, 'SET', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* upsertSocialUser will create or lookup a user as provided from a social
|
||||
* graph.
|
||||
*
|
||||
* @param {Object} ctx the graph context
|
||||
* @param {String} id the ID for the user from the provider
|
||||
* @param {String} provider the name of the provider
|
||||
* @param {String} displayName the users desired displayName, not guaranteed
|
||||
*/
|
||||
static async upsertSocialUser(ctx, id, provider, displayName) {
|
||||
return upsertUser(ctx, id, provider, displayName, 'UNSET');
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user given a social profile and if the user does not exist, creates
|
||||
* them.
|
||||
* @param {Object} profile - User social/external profile
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
static async findOrCreateExternalUser(ctx, id, provider, displayName) {
|
||||
let user = await User.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id,
|
||||
provider,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
ctx.log.warn(
|
||||
'findOrCreateExternalUser is deprecated and will be removed in a future version, replace with upsertExternalUser'
|
||||
);
|
||||
|
||||
// User does not exist and need to be created.
|
||||
|
||||
// Create an initial username for the user.
|
||||
let username = await Users.getInitialUsername(displayName);
|
||||
|
||||
// The user was not found, lets create them!
|
||||
user = new User({
|
||||
username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
profiles: [{ id, provider }],
|
||||
status: {
|
||||
username: {
|
||||
status: 'UNSET',
|
||||
history: {
|
||||
status: 'UNSET',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Save the user in the database.
|
||||
await user.save();
|
||||
|
||||
// Emit that the user was created.
|
||||
ctx.pubsub.publish('userCreated', user);
|
||||
|
||||
return user;
|
||||
return Users.upsertSocialUser(ctx, id, provider, displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user