From 29f20cca24ef0facb3bae97a993670f941c42717 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 30 Oct 2020 22:59:49 +0100 Subject: [PATCH] Make auto marking optional through a setting Fixes #68 --- src/newsreader/accounts/forms.py | 4 +- .../migrations/0013_user_auto_mark_read.py | 20 ++++++++ src/newsreader/accounts/models.py | 9 ++++ src/newsreader/js/pages/homepage/App.js | 9 ++-- .../js/pages/homepage/actions/posts.js | 5 ++ .../js/pages/homepage/components/PostModal.js | 46 +++++++++++++------ src/newsreader/js/pages/homepage/index.js | 9 ++-- .../js/pages/homepage/reducers/posts.js | 8 +++- .../js/tests/homepage/actions/post.test.js | 10 +++- .../js/tests/homepage/reducers/post.test.js | 4 +- src/newsreader/news/core/views.py | 1 + .../scss/components/post/_post.scss | 12 +++-- .../scss/components/sidebar/_sidebar.scss | 4 ++ .../scss/elements/button/_read-button.scss | 2 - 14 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 src/newsreader/accounts/migrations/0013_user_auto_mark_read.py diff --git a/src/newsreader/accounts/forms.py b/src/newsreader/accounts/forms.py index 7a29f99..6247eff 100644 --- a/src/newsreader/accounts/forms.py +++ b/src/newsreader/accounts/forms.py @@ -1,9 +1,11 @@ from django import forms from newsreader.accounts.models import User +from newsreader.core.forms import CheckboxInput class UserSettingsForm(forms.ModelForm): class Meta: model = User - fields = ("first_name", "last_name") + fields = ("first_name", "last_name", "auto_mark_read") + widgets = {"auto_mark_read": CheckboxInput} diff --git a/src/newsreader/accounts/migrations/0013_user_auto_mark_read.py b/src/newsreader/accounts/migrations/0013_user_auto_mark_read.py new file mode 100644 index 0000000..3d975e0 --- /dev/null +++ b/src/newsreader/accounts/migrations/0013_user_auto_mark_read.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-10-27 21:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("accounts", "0012_remove_user_task")] + + operations = [ + migrations.AddField( + model_name="user", + name="auto_mark_read", + field=models.BooleanField( + default=True, + help_text="Wether posts should be marked as read after x amount of seconds of reading", + verbose_name="Auto read marking", + ), + ) + ] diff --git a/src/newsreader/accounts/models.py b/src/newsreader/accounts/models.py index 2451445..c46dd93 100644 --- a/src/newsreader/accounts/models.py +++ b/src/newsreader/accounts/models.py @@ -45,6 +45,15 @@ class User(AbstractUser): twitter_oauth_token = models.CharField(max_length=255, blank=True, null=True) twitter_oauth_token_secret = models.CharField(max_length=255, blank=True, null=True) + # settings + auto_mark_read = models.BooleanField( + _("Auto read marking"), + default=True, + help_text=_( + "Wether posts should be marked as read after x amount of seconds of reading" + ), + ) + username = None objects = UserManager() diff --git a/src/newsreader/js/pages/homepage/App.js b/src/newsreader/js/pages/homepage/App.js index b66d256..0b2aedb 100644 --- a/src/newsreader/js/pages/homepage/App.js +++ b/src/newsreader/js/pages/homepage/App.js @@ -26,10 +26,6 @@ class App extends React.Component { timezone={this.props.timezone} /> - {this.props.error && ( - - )} - {!isEqual(this.props.post, {}) && ( )} + + {this.props.error && ( + + )} ); } diff --git a/src/newsreader/js/pages/homepage/actions/posts.js b/src/newsreader/js/pages/homepage/actions/posts.js index b7ad5cb..f04f3e1 100644 --- a/src/newsreader/js/pages/homepage/actions/posts.js +++ b/src/newsreader/js/pages/homepage/actions/posts.js @@ -9,6 +9,7 @@ export const RECEIVE_POST = 'RECEIVE_POST'; export const REQUEST_POSTS = 'REQUEST_POSTS'; export const MARK_POST_READ = 'MARK_POST_READ'; +export const MARKING_POST = 'MARKING_POST'; export const requestPosts = () => ({ type: REQUEST_POSTS }); @@ -30,10 +31,14 @@ export const postRead = (post, section) => ({ section, }); +export const markingPostRead = () => ({ type: MARKING_POST }); + export const markPostRead = (post, token) => { return (dispatch, getState) => { const { selected } = getState(); + dispatch(markingPostRead()); + const url = `/api/posts/${post.id}/`; const options = { method: 'PATCH', diff --git a/src/newsreader/js/pages/homepage/components/PostModal.js b/src/newsreader/js/pages/homepage/components/PostModal.js index 153f2a8..ab508ae 100644 --- a/src/newsreader/js/pages/homepage/components/PostModal.js +++ b/src/newsreader/js/pages/homepage/components/PostModal.js @@ -21,7 +21,7 @@ class PostModal extends React.Component { const markPostRead = this.props.markPostRead; const token = Cookies.get('csrftoken'); - if (!post.read) { + if (this.props.autoMarking && !post.read) { this.readTimer = setTimeout(markPostRead, 3000, post, token); } @@ -48,27 +48,41 @@ class PostModal extends React.Component { render() { const post = this.props.post; + const token = Cookies.get('csrftoken'); const publicationDate = formatDatetime(post.publicationDate); const titleClassName = post.read ? 'post__title post__title--read' : 'post__title'; - let ruleUrl = ''; + const readButtonDisabled = post.read || this.props.isMarkingPost; - if (this.props.rule.type === SUBREDDIT) { - ruleUrl = `${this.props.subredditUrl}/${this.props.rule.id}/`; - } else if (this.props.rule.type === TWITTER_TIMELINE) { - ruleUrl = `${this.props.timelineUrl}/${this.props.rule.id}/`; - } else { - ruleUrl = `${this.props.feedUrl}/${this.props.rule.id}/`; + let ruleUrl = ''; + switch (this.props.rule.type) { + case SUBREDDIT: + ruleUrl = `${this.props.subredditUrl}/${this.props.rule.id}/`; + break; + case TWITTER_TIMELINE: + ruleUrl = `${this.props.timelineUrl}/${this.props.rule.id}/`; + break; + default: + ruleUrl = `${this.props.feedUrl}/${this.props.rule.id}/`; + break; } return (
- this.props.unSelectPost()} - > - Close - +
+ + +

{`${post.title} `}

@@ -116,4 +130,6 @@ const mapDispatchToProps = dispatch => ({ markPostRead: (post, token) => dispatch(markPostRead(post, token)), }); -export default connect(null, mapDispatchToProps)(PostModal); +const mapStateToProps = state => ({ isMarkingPost: state.posts.isMarking }); + +export default connect(mapStateToProps, mapDispatchToProps)(PostModal); diff --git a/src/newsreader/js/pages/homepage/index.js b/src/newsreader/js/pages/homepage/index.js index 5279482..b934ad3 100644 --- a/src/newsreader/js/pages/homepage/index.js +++ b/src/newsreader/js/pages/homepage/index.js @@ -17,11 +17,12 @@ if (page) { ReactDOM.render( , page diff --git a/src/newsreader/js/pages/homepage/reducers/posts.js b/src/newsreader/js/pages/homepage/reducers/posts.js index 220c59b..608deb2 100644 --- a/src/newsreader/js/pages/homepage/reducers/posts.js +++ b/src/newsreader/js/pages/homepage/reducers/posts.js @@ -5,6 +5,8 @@ import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; import { SELECT_POST, + MARKING_POST, + MARK_POST_READ, RECEIVE_POST, RECEIVE_POSTS, REQUEST_POSTS, @@ -13,7 +15,7 @@ import { SELECT_CATEGORY } from '../actions/categories.js'; import { SELECT_RULE } from '../actions/rules.js'; import { MARK_SECTION_READ } from '../actions/selected.js'; -const defaultState = { items: {}, isFetching: false }; +const defaultState = { items: {}, isFetching: false, isMarking: false }; export const posts = (state = { ...defaultState }, action) => { switch (action.type) { @@ -62,6 +64,10 @@ export const posts = (state = { ...defaultState }, action) => { ...updatedPosts, }, }; + case MARKING_POST: + return { ...state, isMarking: true }; + case MARK_POST_READ: + return { ...state, isMarking: false }; default: return state; } diff --git a/src/newsreader/js/tests/homepage/actions/post.test.js b/src/newsreader/js/tests/homepage/actions/post.test.js index e8f84de..ce2ffdc 100644 --- a/src/newsreader/js/tests/homepage/actions/post.test.js +++ b/src/newsreader/js/tests/homepage/actions/post.test.js @@ -14,12 +14,18 @@ describe('post actions', () => { fetchMock.restore(); }); - it('should create an action request posts', () => { + it('should create an action to request posts', () => { const expectedAction = { type: actions.REQUEST_POSTS }; expect(actions.requestPosts()).toEqual(expectedAction); }); + it('should create an action to mark a post read', () => { + const expectedAction = { type: actions.MARKING_POST }; + + expect(actions.markingPostRead()).toEqual(expectedAction); + }); + it('should create an action receive a post', () => { const post = { id: 2067, @@ -147,6 +153,7 @@ describe('post actions', () => { }); const expectedActions = [ + { type: actions.MARKING_POST }, { type: actions.RECEIVE_POST, post: { ...post, read: true }, @@ -362,6 +369,7 @@ describe('post actions', () => { }); const expectedActions = [ + { type: actions.MARKING_POST }, { type: actions.RECEIVE_POST, post: {} }, { type: errorActions.RECEIVE_API_ERROR, error: TypeError(errorMessage) }, ]; diff --git a/src/newsreader/js/tests/homepage/reducers/post.test.js b/src/newsreader/js/tests/homepage/reducers/post.test.js index ef4234a..6fe728f 100644 --- a/src/newsreader/js/tests/homepage/reducers/post.test.js +++ b/src/newsreader/js/tests/homepage/reducers/post.test.js @@ -12,7 +12,7 @@ describe('post actions', () => { it('should return state after requesting posts', () => { const action = { type: actions.REQUEST_POSTS }; - const expectedState = { ...defaultState, isFetching: true }; + const expectedState = { ...defaultState, isFetching: true, isMarking: false }; expect(reducer(undefined, action)).toEqual(expectedState); }); @@ -40,6 +40,7 @@ describe('post actions', () => { const expectedState = { ...defaultState, isFetching: false, + isMarking: false, items: { [post.id]: post }, }; @@ -85,6 +86,7 @@ describe('post actions', () => { const expectedState = { ...defaultState, isFetching: false, + isMarking: false, items: expectedPosts, }; diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py index 18ec95c..c2ff4d5 100644 --- a/src/newsreader/news/core/views.py +++ b/src/newsreader/news/core/views.py @@ -27,6 +27,7 @@ class NewsView(TemplateView): ), "categoriesUrl": reverse_lazy("news:core:category-update", args=(0,)), "timezone": settings.TIME_ZONE, + "autoMarking": self.request.user.auto_mark_read, }, } diff --git a/src/newsreader/scss/components/post/_post.scss b/src/newsreader/scss/components/post/_post.scss index ae94f6c..7a8ea22 100644 --- a/src/newsreader/scss/components/post/_post.scss +++ b/src/newsreader/scss/components/post/_post.scss @@ -15,6 +15,15 @@ cursor: initial; + &__actions { + display: flex; + justify-content: flex-end; + width: 100%; + + padding: 20px 50px 0; + gap: 20px; + } + &__header { display: flex; flex-direction: column; @@ -81,9 +90,6 @@ } &__close-button { - position: relative; - margin: 1% 2% 0 0; - align-self: flex-end; background-color: $blue; color: $white; diff --git a/src/newsreader/scss/components/sidebar/_sidebar.scss b/src/newsreader/scss/components/sidebar/_sidebar.scss index f13faf3..c70594a 100644 --- a/src/newsreader/scss/components/sidebar/_sidebar.scss +++ b/src/newsreader/scss/components/sidebar/_sidebar.scss @@ -20,4 +20,8 @@ padding: 2px 10px 5px 10px; } } + + .read-button { + margin: 20px 0 0 0; + } } diff --git a/src/newsreader/scss/elements/button/_read-button.scss b/src/newsreader/scss/elements/button/_read-button.scss index 2c345b5..71e8e75 100644 --- a/src/newsreader/scss/elements/button/_read-button.scss +++ b/src/newsreader/scss/elements/button/_read-button.scss @@ -1,8 +1,6 @@ .read-button { @extend .button; - margin: 20px 0 0 0; - color: $white; background-color: $green;