Skip to content

Commit 3ca4b6b

Browse files
authored
feat(posts): remove read articles from feed (#47)
Add filter to the generate feed function to remove read articles from the post
1 parent 97e6b60 commit 3ca4b6b

File tree

9 files changed

+173
-3
lines changed

9 files changed

+173
-3
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
exports.up = knex =>
2+
knex.schema.table('settings', (table) => {
3+
table.boolean('show_only_not_read_posts').defaultTo(false);
4+
});
5+
6+
exports.down = knex =>
7+
knex.schema.table('settings', (table) => {
8+
table.dropColumn('show_only_not_read_posts');
9+
});

src/graphql/helpers/post.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const getFeedParams =
44

55
return {
66
fields: userId ? [...post.defaultUserFields, 'read'] : post.defaultAnonymousFields,
7-
filters: Object.assign({}, { before: args.latest }, filters),
7+
filters: Object.assign({}, { before: args.latest }, filters, typeof args.read === 'boolean' && userId && { read: args.read }),
88
rankBy,
99
userId,
1010
ignoreUserFilters,

src/graphql/schema/post.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export default `
6363
pubs: String
6464
tags: String
6565
sortBy: String
66+
read: Boolean
6667
}
6768
6869
input ToiletInput {

src/models/post.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ const filtersToQuery = async (query, filters = {}, rankBy, userId, ignoreUserFil
249249
* @param {?String[]} filters.postIds Retrieve a set of posts by id
250250
* @param {?Boolean} filters.bookmarks Whether to retrieve only bookmarked posts
251251
* @param {?String} filters.search Text query to filter posts
252+
* @param {?Boolean} filters.read Whether to retrieve only read posts
252253
* @param {?'popularity'|'creation'} rankBy Order criteria
253254
* @param {?String} userId Id of the user who requested the feed
254255
* @param {?Boolean} ignoreUserFilters Whether to ignore the user's preferences
@@ -289,6 +290,20 @@ const generateFeed = async ({
289290
.from(table)
290291
.join('publications', `${table}.publication_id`, 'publications.id');
291292

293+
if (typeof newFilters.read === 'boolean') {
294+
const args = [eventsTable, builder =>
295+
builder.on(`${eventsTable}.post_id`, '=', `${table}.id`)
296+
.andOn(`${eventsTable}.user_id`, '=', db.raw('?', [userId])),
297+
];
298+
if (newFilters.read) {
299+
query = query.join(...args);
300+
query = query.where(`${eventsTable}.type`, '=', 'view');
301+
} else {
302+
query = query.leftJoin(...args);
303+
query = query.whereNull(`${eventsTable}.type`);
304+
}
305+
}
306+
292307
// Join bookmarks table if needed
293308
if (userId && (relevantFields.indexOf('bookmarked') > -1 || (newFilters && newFilters.bookmarks))) {
294309
const args = [bookmarksTable, builder =>

src/models/settings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const getByUserId = userId =>
1313
showTopSites: row.showTopSites === 1,
1414
insaneMode: row.insaneMode === 1,
1515
appInsaneMode: row.appInsaneMode === 1,
16+
showOnlyNotReadPosts: row.showOnlyNotReadPosts === 1,
1617
}))
1718
.then((res) => {
1819
if (res.length) {

test/fixtures/posts.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ const input = [
6060
},
6161
];
6262

63+
const notReadInput = [{ ...input[2], userId: 'user1' }, { ...input[3], userId: 'user1' }, { ...input[4], userId: 'user2' }];
64+
65+
const readInput = [{ ...input[0], userId: 'user1' }, { ...input[3], userId: 'user2' }, { ...input[4], userId: 'user1' }];
66+
6367
const output = [
6468
{
6569
id: input[4].id,
@@ -146,6 +150,32 @@ const outputByCreation = [
146150
output[2],
147151
];
148152

153+
const readOutput = [
154+
{
155+
id: input[4].id,
156+
read: true,
157+
},
158+
{
159+
id: input[0].id,
160+
read: true,
161+
},
162+
];
163+
164+
const notReadOutput = [
165+
{
166+
id: input[4].id,
167+
read: false,
168+
},
169+
{
170+
id: input[1].id,
171+
read: false,
172+
},
173+
{
174+
id: input[0].id,
175+
read: false,
176+
},
177+
];
178+
149179
const bookmarks = [
150180
{ userId: 'user1', postId: input[1].id },
151181
{ userId: 'user1', postId: input[0].id },
@@ -154,10 +184,14 @@ const bookmarks = [
154184

155185
export default {
156186
input,
187+
readInput,
188+
notReadInput,
157189
output,
158190
pubsOutput,
159191
tagsOutput,
160192
searchOutput,
161193
outputByCreation,
194+
readOutput,
195+
notReadOutput,
162196
bookmarks,
163197
};

test/fixtures/settings.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const input = [
44
theme: 'darcula',
55
showTopSites: false,
66
enableCardAnimations: false,
7+
showOnlyNotReadPosts: true,
78
},
89
{
910
userId: 'user2',
@@ -23,6 +24,7 @@ const output = [
2324
insaneMode: false,
2425
appInsaneMode: true,
2526
spaciness: 'eco',
27+
showOnlyNotReadPosts: true,
2628
},
2729
{
2830
userId: 'user2',
@@ -32,6 +34,7 @@ const output = [
3234
insaneMode: false,
3335
appInsaneMode: true,
3436
spaciness: 'roomy',
37+
showOnlyNotReadPosts: false,
3538
},
3639
];
3740

test/integration/graphql/post.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import fixtureToilet from '../../fixtures/toilet';
99
import * as algolia from '../../../src/algolia';
1010
import publication from '../../../src/models/publication';
1111
import post from '../../../src/models/post';
12+
import event from '../../../src/models/event';
1213
import config from '../../../src/config';
1314
import db, { migrate } from '../../../src/db';
1415
import app from '../../../src';
@@ -166,6 +167,70 @@ describe('graphql post', () => {
166167

167168
expect(latest).to.deep.equal(fixture.outputByCreation.map(mapDate));
168169
});
170+
171+
it('should fetch latest not read posts', async () => {
172+
await Promise.all(fixture.input.map(p => post.add(p)));
173+
await Promise.all(fixture.notReadInput.map(p => event.add('view', p.userId, p.id)));
174+
await request.post('/v1/tags/updateCount');
175+
176+
const params = `{
177+
latest: ${JSON.stringify(latestDate)}
178+
page: 0,
179+
pageSize: 20,
180+
read: false,
181+
}`;
182+
183+
const result = await request
184+
.get('/graphql')
185+
.set('Authorization', `Service ${config.accessSecret}`)
186+
.set('User-Id', fixture.notReadInput[0].userId)
187+
.set('Logged-In', true)
188+
.query({
189+
query: `{
190+
latest(params: ${params}) {
191+
id
192+
read
193+
}
194+
}`,
195+
})
196+
.expect(200);
197+
198+
const { latest } = result.body.data;
199+
200+
expect(latest).to.deep.equal(fixture.notReadOutput);
201+
});
202+
203+
it('should fetch latest read posts', async () => {
204+
await Promise.all(fixture.input.map(p => post.add(p)));
205+
await Promise.all(fixture.readInput.map(p => event.add('view', p.userId, p.id)));
206+
await request.post('/v1/tags/updateCount');
207+
208+
const params = `{
209+
latest: ${JSON.stringify(latestDate)}
210+
page: 0,
211+
pageSize: 20,
212+
read: true,
213+
}`;
214+
215+
const result = await request
216+
.get('/graphql')
217+
.set('Authorization', `Service ${config.accessSecret}`)
218+
.set('User-Id', fixture.readInput[0].userId)
219+
.set('Logged-In', true)
220+
.query({
221+
query: `{
222+
latest(params: ${params}) {
223+
id
224+
read
225+
}
226+
}`,
227+
})
228+
.expect(200);
229+
230+
const { latest } = result.body.data;
231+
232+
expect(latest).to.deep.equal(fixture.readOutput);
233+
});
169234
});
170235

171236
describe('get post query', () => {

test/integration/models/post.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,16 @@ describe('post model', () => {
133133
it('should return posts with read field', async () => {
134134
await Promise.all([
135135
event.add('view', '1', feedFixture.posts[0].id),
136-
event.add('view', '1', feedFixture.posts[2].id),
136+
event.add('view', '2', feedFixture.posts[2].id),
137137
event.add('view', '2', feedFixture.posts[1].id),
138+
event.add('view', '1', feedFixture.posts[3].id),
139+
event.add('view', '1', feedFixture.posts[4].id),
138140
]);
139141

140142
const expected = feedFixture.posts.map(p => ({ id: p.id, read: false }));
141143
expected[0].read = true;
142-
expected[2].read = true;
144+
expected[3].read = true;
145+
expected[4].read = true;
143146

144147
const actual = await post.generateFeed({
145148
fields: ['id', 'read'],
@@ -149,6 +152,45 @@ describe('post model', () => {
149152
expect(actual).to.deep.equal(expected);
150153
});
151154

155+
it('should return only read posts', async () => {
156+
await Promise.all([
157+
event.add('view', '1', feedFixture.posts[0].id),
158+
event.add('view', '1', feedFixture.posts[2].id),
159+
event.add('view', '2', feedFixture.posts[1].id),
160+
]);
161+
162+
const expected = [feedFixture.posts[0], feedFixture.posts[2]].map(p => ({ id: p.id }));
163+
164+
const actual = await post.generateFeed({
165+
fields: ['id'],
166+
rankBy: 'creation',
167+
userId: '1',
168+
filters: { read: true },
169+
});
170+
expect(actual).to.deep.equal(expected);
171+
});
172+
173+
it('should return only not read posts', async () => {
174+
await Promise.all([
175+
event.add('view', '1', feedFixture.posts[0].id),
176+
event.add('view', '1', feedFixture.posts[2].id),
177+
event.add('view', '2', feedFixture.posts[1].id),
178+
]);
179+
180+
const readPosts = [feedFixture.posts[0].id, feedFixture.posts[2].id];
181+
const expected = feedFixture.posts
182+
.map(p => ({ id: p.id }))
183+
.filter(({ id }) => !readPosts.includes(id));
184+
185+
const actual = await post.generateFeed({
186+
fields: ['id'],
187+
rankBy: 'creation',
188+
userId: '1',
189+
filters: { read: false },
190+
});
191+
expect(actual).to.deep.equal(expected);
192+
});
193+
152194
it('should return posts within the given time', async () => {
153195
const actual = await post.generateFeed({
154196
fields: ['id'],

0 commit comments

Comments
 (0)