From e8c8aa49a7f6fb49825c16426ddfd871841dae82 Mon Sep 17 00:00:00 2001 From: Daan Wynen Date: Sat, 23 May 2020 05:44:26 +0200 Subject: [PATCH 1/2] Add simple pagination to user feeds. This is simply based on sequence numbers and the `gt`/`lt` arguments of `createUserStream`. --- src/index.js | 25 +++++++++++++++++++++-- src/models.js | 42 ++++++++++++++++++++++++++++++++++---- src/views/i18n.js | 7 +++++++ src/views/index.js | 50 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/index.js b/src/index.js index d340f9c..2608009 100755 --- a/src/index.js +++ b/src/index.js @@ -264,11 +264,20 @@ router }) .get("/author/:feed", async (ctx) => { const { feed } = ctx.params; + + const gt = Number(ctx.request.query['gt'] || -1); + const lt = Number(ctx.request.query['lt'] || -1); + + if (lt > 0 && gt > 0 && gt >= lt) + throw new Error("Given search range is empty"); + const author = async (feedId) => { const description = await about.description(feedId); const name = await about.name(feedId); const image = await about.image(feedId); - const messages = await post.fromPublicFeed(feedId); + const messages = await post.fromPublicFeed(feedId, gt, lt); + const firstPost = await post.firstBy(feedId); + const lastPost = await post.latestBy(feedId); const relationship = await friend.getRelationship(feedId); const avatarUrl = `/image/256/${encodeURIComponent(image)}`; @@ -276,6 +285,8 @@ router return authorView({ feedId, messages, + firstPost, + lastPost, name, description, avatarUrl, @@ -342,17 +353,27 @@ router .get("/profile/", async (ctx) => { const myFeedId = await meta.myFeedId(); + const gt = Number(ctx.request.query['gt'] || -1); + const lt = Number(ctx.request.query['lt'] || -1); + + if (lt > 0 && gt > 0 && gt >= lt) + throw new Error("Given search range is empty"); + const description = await about.description(myFeedId); const name = await about.name(myFeedId); const image = await about.image(myFeedId); - const messages = await post.fromPublicFeed(myFeedId); + const messages = await post.fromPublicFeed(myFeedId, gt, lt); + const firstPost = await post.firstBy(myFeedId); + const lastPost = await post.latestBy(myFeedId); const avatarUrl = `/image/256/${encodeURIComponent(image)}`; ctx.body = await authorView({ feedId: myFeedId, messages, + firstPost, + lastPost, name, description, avatarUrl, diff --git a/src/models.js b/src/models.js index 8b8a5aa..16db284 100644 --- a/src/models.js +++ b/src/models.js @@ -627,14 +627,45 @@ module.exports = ({ cooler, isPublic }) => { }) ); + const getLimitPost = async (feedId, reverse) => { + const ssb = await cooler.open(); + const source = ssb.createUserStream({ id: feedId, reverse: reverse }); + const messages = await new Promise((resolve, reject) => { + pull( + source, + pull.filter((msg) => isDecrypted(msg) === false && isPost(msg)), + pull.take(1), + pull.collect((err, collectedMessages) => { + if (err) { + reject(err); + } else { + resolve(transform(ssb, collectedMessages, feedId)); + } + }) + ); + }); + return messages.length ? messages[0] : undefined; + }; + const post = { - fromPublicFeed: async (feedId, customOptions = {}) => { + firstBy: async (feedId) => { + return getLimitPost(feedId, false); + }, + latestBy: async (feedId) => { + return getLimitPost(feedId, true); + }, + fromPublicFeed: async (feedId, gt = -1, lt = -1, customOptions = {}) => { const ssb = await cooler.open(); const myFeedId = ssb.id; - const options = configure({ id: feedId }, customOptions); - + let defaultOptions = { id: feedId }; + if (lt >= 0) + defaultOptions.lt = lt; + if (gt >= 0) + defaultOptions.gt = gt; + defaultOptions.reverse = !(gt >= 0 && lt < 0); + const options = configure(defaultOptions, customOptions); const { blocking } = await models.friend.getRelationship(feedId); // Avoid streaming any messages from this feed. If we used the social @@ -661,7 +692,10 @@ module.exports = ({ cooler, isPublic }) => { ); }); - return messages; + if (!defaultOptions.reverse) + return messages.reverse(); + else + return messages; }, mentionsMe: async (customOptions = {}) => { const ssb = await cooler.open(); diff --git a/src/views/i18n.js b/src/views/i18n.js index d5f2ed5..7592a08 100644 --- a/src/views/i18n.js +++ b/src/views/i18n.js @@ -62,6 +62,13 @@ const i18n = { follow: "Follow", block: "Block", unblock: "Unblock", + newerPosts: "Newer posts", + olderPosts: "Older posts", + feedRangeEmpty: "The given range is empty for this feed. Try viewing the ", + seeFullFeed: "full feed", + feedEmpty: "The local client has never seen posts from this account.", + beginningOfFeed: "This is the beginning of the feed", + noNewerPosts: "No newer posts have been reveived yet.", relationshipFollowing: "You are following", relationshipYou: "This is you", relationshipBlocking: "You are blocking", diff --git a/src/views/index.js b/src/views/index.js index cea70ac..fd514be 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -512,6 +512,8 @@ exports.authorView = ({ description, feedId, messages, + firstPost, + lastPost, name, relationship, }) => { @@ -600,10 +602,56 @@ exports.authorView = ({ ) ); + const linkUrl = relationship.me ? '/profile/' : `/author/${encodeURIComponent(feedId)}/`; + + let items = messages.map((msg) => post({ msg })); + if (items.length === 0) { + if (lastPost === undefined) { + items.push( + section( + div( + span(i18n.feedEmpty) + ) + ) + ); + } else { + items.push( + section( + div( + span(i18n.feedRangeEmpty), + a({ href: `${linkUrl}` }, i18n.seeFullFeed) + ) + ) + ); + } + } else { + const highestSeqNum = messages[0].value.sequence; + const lowestSeqNum = messages[messages.length-1].value.sequence; + let newerPostsLink; + if (lastPost !== undefined && highestSeqNum < lastPost.value.sequence) + newerPostsLink = a({ href: `${linkUrl}?gt=${highestSeqNum}` }, i18n.newerPosts); + else + newerPostsLink = span(i18n.newerPosts, { title: i18n.noNewerPosts }); + let olderPostsLink; + if (lowestSeqNum > firstPost.value.sequence) + olderPostsLink = a({ href: `${linkUrl}?lt=${lowestSeqNum}` }, i18n.olderPosts); + else + olderPostsLink = span(i18n.olderPosts, { title: i18n.beginningOfFeed }); + const pagination = section( + { class: "message" }, + footer( + div(newerPostsLink, olderPostsLink), + br() + ) + ); + items.unshift(pagination); + items.push(pagination); + } + return template( i18n.profile, prefix, - messages.map((msg) => post({ msg })) + items ); }; From a40b2eefc1868416effee96d254dcc0a595450ba Mon Sep 17 00:00:00 2001 From: Daan Wynen Date: Sat, 23 May 2020 20:48:43 +0200 Subject: [PATCH 2/2] make the linters happy. this time they had a *lot* of opinions. :P --- src/index.js | 8 ++++---- src/models.js | 12 ++++-------- src/views/i18n.js | 2 +- src/views/index.js | 40 +++++++++++++++++----------------------- test/basic.js | 2 ++ 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/index.js b/src/index.js index 2608009..26914b3 100755 --- a/src/index.js +++ b/src/index.js @@ -265,8 +265,8 @@ router .get("/author/:feed", async (ctx) => { const { feed } = ctx.params; - const gt = Number(ctx.request.query['gt'] || -1); - const lt = Number(ctx.request.query['lt'] || -1); + const gt = Number(ctx.request.query["gt"] || -1); + const lt = Number(ctx.request.query["lt"] || -1); if (lt > 0 && gt > 0 && gt >= lt) throw new Error("Given search range is empty"); @@ -353,8 +353,8 @@ router .get("/profile/", async (ctx) => { const myFeedId = await meta.myFeedId(); - const gt = Number(ctx.request.query['gt'] || -1); - const lt = Number(ctx.request.query['lt'] || -1); + const gt = Number(ctx.request.query["gt"] || -1); + const lt = Number(ctx.request.query["lt"] || -1); if (lt > 0 && gt > 0 && gt >= lt) throw new Error("Given search range is empty"); diff --git a/src/models.js b/src/models.js index 16db284..08d6db0 100644 --- a/src/models.js +++ b/src/models.js @@ -660,10 +660,8 @@ module.exports = ({ cooler, isPublic }) => { const myFeedId = ssb.id; let defaultOptions = { id: feedId }; - if (lt >= 0) - defaultOptions.lt = lt; - if (gt >= 0) - defaultOptions.gt = gt; + if (lt >= 0) defaultOptions.lt = lt; + if (gt >= 0) defaultOptions.gt = gt; defaultOptions.reverse = !(gt >= 0 && lt < 0); const options = configure(defaultOptions, customOptions); const { blocking } = await models.friend.getRelationship(feedId); @@ -692,10 +690,8 @@ module.exports = ({ cooler, isPublic }) => { ); }); - if (!defaultOptions.reverse) - return messages.reverse(); - else - return messages; + if (!defaultOptions.reverse) return messages.reverse(); + else return messages; }, mentionsMe: async (customOptions = {}) => { const ssb = await cooler.open(); diff --git a/src/views/i18n.js b/src/views/i18n.js index 7592a08..58bb3b7 100644 --- a/src/views/i18n.js +++ b/src/views/i18n.js @@ -68,7 +68,7 @@ const i18n = { seeFullFeed: "full feed", feedEmpty: "The local client has never seen posts from this account.", beginningOfFeed: "This is the beginning of the feed", - noNewerPosts: "No newer posts have been reveived yet.", + noNewerPosts: "No newer posts have been received yet.", relationshipFollowing: "You are following", relationshipYou: "This is you", relationshipBlocking: "You are blocking", diff --git a/src/views/index.js b/src/views/index.js index fd514be..d0e0992 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -505,7 +505,7 @@ exports.editProfileView = ({ name, description }) => ); /** - * @param {{avatarUrl: string, description: string, feedId: string, messages: any[], name: string, relationship: object}} input + * @param {{avatarUrl: string, description: string, feedId: string, messages: any[], name: string, relationship: object, firstPost: object, lastPost: object}} input */ exports.authorView = ({ avatarUrl, @@ -602,18 +602,14 @@ exports.authorView = ({ ) ); - const linkUrl = relationship.me ? '/profile/' : `/author/${encodeURIComponent(feedId)}/`; + const linkUrl = relationship.me + ? "/profile/" + : `/author/${encodeURIComponent(feedId)}/`; let items = messages.map((msg) => post({ msg })); if (items.length === 0) { if (lastPost === undefined) { - items.push( - section( - div( - span(i18n.feedEmpty) - ) - ) - ); + items.push(section(div(span(i18n.feedEmpty)))); } else { items.push( section( @@ -626,33 +622,31 @@ exports.authorView = ({ } } else { const highestSeqNum = messages[0].value.sequence; - const lowestSeqNum = messages[messages.length-1].value.sequence; + const lowestSeqNum = messages[messages.length - 1].value.sequence; let newerPostsLink; if (lastPost !== undefined && highestSeqNum < lastPost.value.sequence) - newerPostsLink = a({ href: `${linkUrl}?gt=${highestSeqNum}` }, i18n.newerPosts); - else - newerPostsLink = span(i18n.newerPosts, { title: i18n.noNewerPosts }); + newerPostsLink = a( + { href: `${linkUrl}?gt=${highestSeqNum}` }, + i18n.newerPosts + ); + else newerPostsLink = span(i18n.newerPosts, { title: i18n.noNewerPosts }); let olderPostsLink; if (lowestSeqNum > firstPost.value.sequence) - olderPostsLink = a({ href: `${linkUrl}?lt=${lowestSeqNum}` }, i18n.olderPosts); + olderPostsLink = a( + { href: `${linkUrl}?lt=${lowestSeqNum}` }, + i18n.olderPosts + ); else olderPostsLink = span(i18n.olderPosts, { title: i18n.beginningOfFeed }); const pagination = section( { class: "message" }, - footer( - div(newerPostsLink, olderPostsLink), - br() - ) + footer(div(newerPostsLink, olderPostsLink), br()) ); items.unshift(pagination); items.push(pagination); } - return template( - i18n.profile, - prefix, - items - ); + return template(i18n.profile, prefix, items); }; exports.commentView = async ({ messages, myFeedId, parentMessage }) => { diff --git a/test/basic.js b/test/basic.js index efcd09c..ef50631 100644 --- a/test/basic.js +++ b/test/basic.js @@ -10,6 +10,8 @@ const paths = [ "/inbox", "/mentions", "/profile", + "/profile?gt=0", + "/profile?lt=100", "/profile/edit", "/public/latest", "/public/latest/extended",