Compare commits
7 commits
main
...
optional-a
| Author | SHA1 | Date | |
|---|---|---|---|
| f0a0ad776d | |||
| 66c8ab1a8c | |||
| 36a3b54b7b | |||
| d60b01faa3 | |||
| 2b76c560c2 | |||
| 67e06ee50e | |||
| c6e69496cf |
14 changed files with 111 additions and 32 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
)
|
||||
]
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -26,10 +26,6 @@ class App extends React.Component {
|
|||
timezone={this.props.timezone}
|
||||
/>
|
||||
|
||||
{this.props.error && (
|
||||
<Messages messages={[{ type: 'error', text: this.props.error.message }]} />
|
||||
)}
|
||||
|
||||
{!isEqual(this.props.post, {}) && (
|
||||
<PostModal
|
||||
post={this.props.post}
|
||||
|
|
@ -40,8 +36,13 @@ class App extends React.Component {
|
|||
timelineUrl={this.props.timelineUrl}
|
||||
categoriesUrl={this.props.categoriesUrl}
|
||||
timezone={this.props.timezone}
|
||||
autoMarking={this.props.autoMarking}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.props.error && (
|
||||
<Messages messages={[{ type: 'error', text: this.props.error.message }]} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="modal post-modal">
|
||||
<div className="post">
|
||||
<span
|
||||
className="button post__close-button"
|
||||
onClick={() => this.props.unSelectPost()}
|
||||
>
|
||||
Close <i className="gg-close"></i>
|
||||
</span>
|
||||
<div className="post__actions">
|
||||
<button
|
||||
className={`button read-button ${readButtonDisabled && 'button--disabled'}`}
|
||||
onClick={() => !readButtonDisabled && this.props.markPostRead(post, token)}
|
||||
>
|
||||
Mark as read
|
||||
</button>
|
||||
<button
|
||||
className="button post__close-button"
|
||||
onClick={() => this.props.unSelectPost()}
|
||||
>
|
||||
Close <i className="gg-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="post__header">
|
||||
<h2 className={titleClassName}>{`${post.title} `}</h2>
|
||||
<div className="post__meta-info">
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ if (page) {
|
|||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App
|
||||
feedUrl={feedUrl.substring(1, feedUrl.length - 4)}
|
||||
subredditUrl={subredditUrl.substring(1, subredditUrl.length - 4)}
|
||||
timelineUrl={timelineUrl.substring(1, timelineUrl.length - 4)}
|
||||
categoriesUrl={categoriesUrl.substring(1, categoriesUrl.length - 4)}
|
||||
feedUrl={feedUrl.substring(1, feedUrl.length - 3)}
|
||||
subredditUrl={subredditUrl.substring(1, subredditUrl.length - 3)}
|
||||
timelineUrl={timelineUrl.substring(1, timelineUrl.length - 3)}
|
||||
categoriesUrl={categoriesUrl.substring(1, categoriesUrl.length - 3)}
|
||||
timezone={settings.timezone}
|
||||
autoMarking={settings.autoMarking}
|
||||
/>
|
||||
</Provider>,
|
||||
page
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -20,4 +20,8 @@
|
|||
padding: 2px 10px 5px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.read-button {
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
.read-button {
|
||||
@extend .button;
|
||||
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
color: $white;
|
||||
background-color: $green;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue