diff --git a/src/index.js b/src/index.js index d340f9c..26914b3 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..08d6db0 100644 --- a/src/models.js +++ b/src/models.js @@ -627,14 +627,43 @@ 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 +690,8 @@ 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..58bb3b7 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 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 cea70ac..d0e0992 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -505,13 +505,15 @@ 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, description, feedId, messages, + firstPost, + lastPost, name, relationship, }) => { @@ -600,11 +602,51 @@ exports.authorView = ({ ) ); - return template( - i18n.profile, - prefix, - messages.map((msg) => post({ msg })) - ); + 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, 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",