diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js
index 16c2bd2f7..a46d2dccd 100644
--- a/client/coral-admin/src/actions/moderation.js
+++ b/client/coral-admin/src/actions/moderation.js
@@ -5,14 +5,7 @@ export const singleView = () => ({ type: actions.SINGLE_VIEW });
// hide shortcuts note
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
- try {
- if (localStorage) {
- localStorage.setItem('coral:shortcutsNote', 'hide');
- }
- } catch (e) {
- // above will fail in Safari private mode
- }
-
+ localStorage.setItem('coral:shortcutsNote', 'hide');
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
};
diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js
index 0cf0a8cf9..158870dec 100644
--- a/client/coral-admin/src/index.js
+++ b/client/coral-admin/src/index.js
@@ -15,7 +15,8 @@ import { hideShortcutsNote } from './actions/moderation';
smoothscroll.polyfill();
function init({ store, localStorage }) {
- if (localStorage && localStorage.getItem('coral:shortcutsNote') === 'hide') {
+ const shouldHide = localStorage.getItem('coral:shortcutsNote') === 'hide';
+ if (shouldHide) {
store.dispatch(hideShortcutsNote());
}
}
diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js
index 5390153e6..5e3b0846f 100644
--- a/client/coral-framework/actions/auth.js
+++ b/client/coral-framework/actions/auth.js
@@ -15,9 +15,7 @@ export const checkLogin = () => (
rest('/auth')
.then(result => {
if (!result.user) {
- if (localStorage) {
- cleanAuthData(localStorage);
- }
+ cleanAuthData(localStorage);
dispatch(checkLoginSuccess(null));
return;
}
@@ -52,10 +50,8 @@ const checkLoginSuccess = user => ({
});
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
- if (localStorage) {
- localStorage.setItem('exp', jwtDecode(token).exp);
- localStorage.setItem('token', token);
- }
+ localStorage.setItem('exp', jwtDecode(token).exp);
+ localStorage.setItem('token', token);
// Dispatch the set auth token action. For some browsers and situations, we
// may not be able to persist the auth token any other way. Keep it in redux!
@@ -70,11 +66,8 @@ export const handleSuccessfulLogin = (user, token) => (
{ client, localStorage, postMessage }
) => {
const { exp } = jwtDecode(token);
-
- if (localStorage) {
- localStorage.setItem('exp', exp);
- localStorage.setItem('token', token);
- }
+ localStorage.setItem('exp', exp);
+ localStorage.setItem('token', token);
// Send the message via the messages service to the window.opener if it
// exists.
@@ -105,9 +98,8 @@ export const logout = () => async (
) => {
await rest('/auth', { method: 'DELETE' });
- if (localStorage) {
- cleanAuthData(localStorage);
- }
+ // Clear the auth data persisted to localStorage.
+ cleanAuthData(localStorage);
// Reset the websocket.
client.resetWebsocket();
diff --git a/client/coral-framework/services/i18n.js b/client/coral-framework/services/i18n.js
index 9c65c9c4e..e1c3ab99e 100644
--- a/client/coral-framework/services/i18n.js
+++ b/client/coral-framework/services/i18n.js
@@ -52,26 +52,41 @@ let lang;
let timeagoInstance;
function setLocale(storage, locale) {
- try {
- if (storage) {
- storage.setItem('locale', locale);
- }
- } catch (err) {
- console.error(err);
- }
+ storage.setItem('locale', locale);
}
-function getLocale(storage) {
+// 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.
+function detectLanguage(storage) {
try {
- return (
- (storage && storage.getItem('locale')) ||
- navigator.language ||
- defaultLanguage
- ).split('-')[0];
+ const lang = storage.getItem('locale') || navigator.language;
+ if (lang) {
+ return lang;
+ }
} catch (err) {
- console.error(err);
- return null;
+ console.warn(
+ 'Error while trying to detect language, will fallback to',
+ err
+ );
}
+
+ console.warn('Could not detect language, will fallback to', defaultLanguage);
+ return defaultLanguage;
+}
+
+// getLocale will get the users locale from the local detector and parse it to a
+// format we can work with.
+function getLocale(storage) {
+ // Get the language from the local detector.
+ const lang = detectLanguage(storage);
+
+ // Some language strings come with additional subtags as defined in:
+ //
+ // https://www.ietf.org/rfc/bcp/bcp47.txt
+ //
+ // So we should strip that off if we find it.
+ return lang.split('-')[0];
}
export function setupTranslations() {
diff --git a/client/coral-framework/services/storage.js b/client/coral-framework/services/storage.js
index 66e2c96cd..7b7acf655 100644
--- a/client/coral-framework/services/storage.js
+++ b/client/coral-framework/services/storage.js
@@ -1,40 +1,116 @@
import uuid from 'uuid/v4';
-function getStorage(type) {
- let storage;
- try {
- storage = window[type];
- const x = '__storage_test__';
- storage.setItem(x, x);
- storage.removeItem(x);
- } catch (e) {
- const ignore =
- e instanceof DOMException &&
- // everything except Firefox
- (e.code === 22 ||
- // SecurityError related to having 3rd party cookies disabled.
- e.code === 18 ||
- // Firefox
+function testStorageAccess(storage) {
+ const key = '__storage_test__';
- e.code === 1014 ||
- // test name field too, because code might not be present
+ // Create a unique test value.
+ const expectedValue = String(Date.now());
- // everything except Firefox
- e.name === 'QuotaExceededError' ||
- // Firefox
- e.name === 'NS_ERROR_DOM_QUOTA_REACHED');
- if (!ignore) {
- console.warn(e);
+ // Try to set, get, and remove that item.
+ storage.setItem(key, expectedValue);
+ const canSetGet = expectedValue === storage.getItem(key);
+ storage.removeItem(key);
+
+ if (!canSetGet) {
+ // We can't access the desired storage!
+ throw new Error('Storage access test failed');
+ }
+}
+
+// InMemoryStorage is a dumb implementation of the Storage interface that will
+// not persist the data at all. It implements the Storage interface found:
+//
+// https://developer.mozilla.org/en-US/docs/Web/API/Storage
+//
+class InMemoryStorage {
+ constructor() {
+ this.storage = {};
+ }
+
+ get length() {
+ return Object.keys(this.storage).length;
+ }
+
+ key(n) {
+ if (this.length <= n) {
+ return undefined;
}
- // When third party cookies are disabled, session storage is readable/
- // writable, but localStorage is not. Try to get the sessionStorage to use.
- if (type !== 'sessionStorage') {
- return getStorage('sessionStorage');
+ return this.storage[Object.keys(this.storage)[n]];
+ }
+
+ getItem(key) {
+ return this.storage[key];
+ }
+
+ setItem(key, value) {
+ this.storage[key] = value;
+
+ try {
+ // Test sessionStorage. We could have been given access recently.
+ testStorageAccess(sessionStorage);
+
+ // Test passed! Set the item in sessionStorage.
+ sessionStorage.setItem(key, value);
+ console.log(
+ 'Attempt to persist InMemoryStorage value to sessionStorage succeeded'
+ );
+ } catch (err) {
+ console.warn(
+ 'Attempt to persist InMemoryStorage value to sessionStorage failed',
+ err
+ );
}
}
- return storage;
+ removeItem(key) {
+ delete this.storage[key];
+
+ try {
+ // Test sessionStorage. We could have been given access recently.
+ testStorageAccess(sessionStorage);
+
+ // Test passed! Remove the item from sessionStorage.
+ sessionStorage.removeItem(key);
+ console.log(
+ 'Attempt to persist InMemoryStorage delete to sessionStorage succeeded'
+ );
+ } catch (err) {
+ console.warn(
+ 'Attempt to persist InMemoryStorage delete to sessionStorage failed',
+ err
+ );
+ }
+ }
+}
+
+// getStorage will test to see if the requested storage type is available, if it
+// is not, it will try sessionStorage, and if that is also not available, it
+// will fallback to InMemoryStorage.
+function getStorage(type) {
+ try {
+ // Get the desired storage from the window and test it out.
+ const storage = window[type];
+ testStorageAccess(storage);
+
+ // Storage test was successful! Return it.
+ return storage;
+ } catch (err) {
+ // When third party cookies are disabled, session storage is readable/
+ // writable, but localStorage is not. Try to get the sessionStorage to use.
+ if (type !== 'sessionStorage') {
+ console.warn('Could not access', type, 'trying sessionStorage', err);
+ return getStorage('sessionStorage');
+ }
+
+ console.warn(
+ 'Could not access sessionStorage falling back to InMemoryStorage',
+ err
+ );
+ }
+
+ // No acceptable storage could be found, returning the InMemoryStorage.
+ return new InMemoryStorage();
}
/**
diff --git a/client/coral-ui/components/TextField.js b/client/coral-ui/components/TextField.js
index 6cbb7a06b..2ba23e6e8 100644
--- a/client/coral-ui/components/TextField.js
+++ b/client/coral-ui/components/TextField.js
@@ -27,11 +27,14 @@ const TextField = ({
);
TextField.propTypes = {
+ id: PropTypes.string,
label: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
errorMsg: PropTypes.string,
type: PropTypes.string,
+ className: PropTypes.string,
+ showErrors: PropTypes.bool,
};
export default TextField;
diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js
index 46fe0ac3c..67478785a 100644
--- a/graph/resolvers/user.js
+++ b/graph/resolvers/user.js
@@ -29,9 +29,9 @@ const User = {
return Comments.getByQuery(query);
},
- ignoredUsers({ ignoresUsers }, args, { user, loaders: { Users } }) {
+ ignoredUsers({ ignoresUsers }, args, { loaders: { Users } }) {
// Return nothing if there is nothing to query for.
- if (!user.ignoresUsers || user.ignoresUsers.length <= 0) {
+ if (!ignoresUsers || ignoresUsers.length <= 0) {
return [];
}
diff --git a/package.json b/package.json
index b0a37528e..845e2b11a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "talk",
- "version": "4.3.1",
+ "version": "4.3.2",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
diff --git a/plugins/talk-plugin-auth/client/login/components/SignUp.js b/plugins/talk-plugin-auth/client/login/components/SignUp.js
index cc49e7391..83f81f81d 100644
--- a/plugins/talk-plugin-auth/client/login/components/SignUp.js
+++ b/plugins/talk-plugin-auth/client/login/components/SignUp.js
@@ -75,6 +75,7 @@ class SignUp extends React.Component {
showErrors={!!emailError}
errorMsg={emailError}
onChange={this.handleEmailChange}
+ autocomplete="off"
/>
{passwordError && (
@@ -113,6 +117,7 @@ class SignUp extends React.Component {
errorMsg={passwordRepeatError}
onChange={this.handlePasswordRepeatChange}
minLength="8"
+ autocomplete="off"
/>
{
@@ -23,6 +23,9 @@ const handle = async (ctx, comment) => {
id
user {
id
+ ignoredUsers {
+ id
+ }
notificationSettings {
onReply
}
@@ -53,13 +56,23 @@ const handle = async (ctx, comment) => {
return;
}
+ // Pull out the author of the new comment.
+ const authorID = get(comment, 'author_id');
+
// Check to see if this is yourself replying to yourself, if that's the case
// don't send a notification.
- if (userID === get(comment, 'author_id')) {
+ if (userID === authorID) {
ctx.log.info('user id of parent comment is the same as the new comment');
return;
}
+ // Check to see if this user is ignoring the user who replied to their
+ // comment.
+ if (map(get(comment, 'user.ignoredUsers', []), 'id').indexOf(authorID)) {
+ ctx.log.info('parent user has ignored the author of the new comment');
+ return;
+ }
+
// The user does have notifications for replied comments enabled, queue the
// notification to be sent.
return { userID, date: comment.created_at, context: comment.id };