diff --git a/src/lib/collectors.js b/src/lib/collectors.js index f2a481b..79f806a 100644 --- a/src/lib/collectors.js +++ b/src/lib/collectors.js @@ -12,7 +12,7 @@ const requestCache = new RequestCache(constants.caching.resource_cache_time) const userRequestCache = new UserRequestCache(constants.caching.resource_cache_time) /** @type {import("./cache").TtlCache} */ const timelineEntryCache = new TtlCache(constants.caching.resource_cache_time) -const history = new RequestHistory(["user", "timeline", "post", "reel"]) +const history = new RequestHistory(["user", "timeline", "igtv", "post", "reel"]) const AssistantSwitcher = require("./structures/AssistantSwitcher") const assistantSwitcher = new AssistantSwitcher() @@ -306,12 +306,12 @@ function fetchIGTVPage(userID, after) { if (res.status === 429) throw constants.symbols.RATE_LIMITED }).then(g => g.json()).then(root => { /** @type {import("./types").PagedEdges} */ - const timeline = root.data.user.edge_owner_to_timeline_media - history.report("timeline", true) + const timeline = root.data.user.edge_felix_video_timeline + history.report("igtv", true) return timeline }).catch(error => { if (error === constants.symbols.RATE_LIMITED) { - history.report("timeline", false) + history.report("igtv", false) } throw error }) @@ -422,6 +422,7 @@ function fetchShortcodeData(shortcode) { module.exports.fetchUser = fetchUser module.exports.fetchTimelinePage = fetchTimelinePage +module.exports.fetchIGTVPage = fetchIGTVPage module.exports.getOrCreateShortcode = getOrCreateShortcode module.exports.fetchShortcodeData = fetchShortcodeData module.exports.userRequestCache = userRequestCache diff --git a/src/lib/structures/ReelUser.js b/src/lib/structures/ReelUser.js index 95ae382..68d6c4b 100644 --- a/src/lib/structures/ReelUser.js +++ b/src/lib/structures/ReelUser.js @@ -12,8 +12,8 @@ class ReelUser extends BaseUser { this.posts = 0 this.following = data.edge_follow ? data.edge_follow.count : 0 this.followedBy = data.edge_followed_by ? data.edge_followed_by.count : 0 - /** @type {import("./Timeline")} */ - this.timeline = new Timeline(this) + this.timeline = new Timeline(this, "timeline") + this.igtv = new Timeline(this, "igtv") this.cachedAt = Date.now() this.computeProxyProfilePic() } diff --git a/src/lib/structures/Timeline.js b/src/lib/structures/Timeline.js index 6f28251..6616d3c 100644 --- a/src/lib/structures/Timeline.js +++ b/src/lib/structures/Timeline.js @@ -21,9 +21,12 @@ function transformEdges(edges) { class Timeline { /** * @param {import("./User")|import("./ReelUser")} user + * @param {string} type */ - constructor(user) { + constructor(user, type) { this.user = user + /** one of: "timeline", "igtv" */ + this.type = type /** @type {import("./TimelineEntry")[][]} */ this.pages = [] if (this.user.data.edge_owner_to_timeline_media) { @@ -32,12 +35,17 @@ class Timeline { } hasNextPage() { - return this.page_info.has_next_page + return !this.page_info || this.page_info.has_next_page } fetchNextPage() { if (!this.hasNextPage()) return constants.symbols.NO_MORE_PAGES - return collectors.fetchTimelinePage(this.user.data.id, this.page_info.end_cursor).then(page => { + const method = + this.type === "timeline" ? collectors.fetchTimelinePage + : this.type === "igtv" ? collectors.fetchIGTVPage + : null + const after = this.page_info ? this.page_info.end_cursor : "" + return method(this.user.data.id, after).then(page => { this.addPage(page) return this.pages.slice(-1)[0] }) diff --git a/src/lib/structures/TimelineEntry.js b/src/lib/structures/TimelineEntry.js index a66f710..1e9b878 100644 --- a/src/lib/structures/TimelineEntry.js +++ b/src/lib/structures/TimelineEntry.js @@ -172,13 +172,23 @@ class TimelineEntry extends TimelineBaseMethods { config_height: found.config_height, src: proxyImage(found.src, found.config_width) // force resize to config rather than requested } + } else if (this.data.thumbnail_src) { + return { + config_width: size, // probably? + config_height: size, + src: proxyImage(this.data.thumbnail_src, size) // force resize to requested + } } else { return null } } getThumbnailSizes() { - return `(max-width: 820px) 200px, 260px` // from css :( + if (this.data.thumbnail_resources) { + return `(max-width: 820px) 200px, 260px` // from css :( + } else { + return null + } } async fetchChildren() { diff --git a/src/lib/structures/User.js b/src/lib/structures/User.js index a9ac566..dd6285c 100644 --- a/src/lib/structures/User.js +++ b/src/lib/structures/User.js @@ -13,7 +13,8 @@ class User extends BaseUser { this.following = data.edge_follow.count this.followedBy = data.edge_followed_by.count this.posts = data.edge_owner_to_timeline_media.count - this.timeline = new Timeline(this) + this.timeline = new Timeline(this, "timeline") + this.igtv = new Timeline(this, "igtv") this.cachedAt = Date.now() this.computeProxyProfilePic() } diff --git a/src/site/api/routes.js b/src/site/api/routes.js index 21fcbf7..923d8c8 100644 --- a/src/site/api/routes.js +++ b/src/site/api/routes.js @@ -65,22 +65,27 @@ module.exports = [ } }, { - route: `/u/(${constants.external.username_regex})`, methods: ["GET"], code: ({req, url, fill}) => { - if (fill[0] !== fill[0].toLowerCase()) { // some capital letters - return Promise.resolve(redirect(`/u/${fill[0].toLowerCase()}`, 301)) + route: `/u/(${constants.external.username_regex})(/channel)?`, methods: ["GET"], code: ({req, url, fill}) => { + const username = fill[0] + const type = fill[1] ? "igtv" : "timeline" + + if (username !== username.toLowerCase()) { // some capital letters + return Promise.resolve(redirect(`/u/${username.toLowerCase()}`, 301)) } const settings = getSettings(req) const params = url.searchParams - return fetchUser(fill[0]).then(async user => { - const page = +params.get("page") - if (typeof page === "number" && !isNaN(page) && page >= 1) { - await user.timeline.fetchUpToPage(page - 1) - } + return fetchUser(username).then(async user => { + const selectedTimeline = user[type] + let pageNumber = +params.get("page") + if (isNaN(pageNumber) || pageNumber < 1) pageNumber = 1 + await selectedTimeline.fetchUpToPage(pageNumber - 1) const followerCountsAvailable = !(user.constructor.name === "ReelUser" && user.following === 0 && user.followedBy === 0) return render(200, "pug/user.pug", { url, user, + selectedTimeline, + type, followerCountsAvailable, constants, settings, @@ -100,12 +105,12 @@ module.exports = [ statusCode: 503, contentType: "text/html", headers: { - "Retry-After": userRequestCache.getTtl("user/"+fill[0], 1000) + "Retry-After": userRequestCache.getTtl("user/"+username, 1000) }, content: pugCache.get("pug/blocked.pug").web({ website_origin: constants.website_origin, - username: fill[0], - expiresMinutes: userRequestCache.getTtl("user/"+fill[0], 1000*60), + username, + expiresMinutes: userRequestCache.getTtl("user/"+username, 1000*60), getStaticURL, settings }) @@ -120,13 +125,25 @@ module.exports = [ }, { route: `/fragment/user/(${constants.external.username_regex})/(\\d+)`, methods: ["GET"], code: async ({req, url, fill}) => { + const username = fill[0] + let pageNumber = +fill[1] + if (isNaN(pageNumber) || pageNumber < 1) { + return { + statusCode: 400, + contentType: "text/html", + content: "Bad page number" + } + } + let type = url.searchParams.get("type") + if (!["timeline", "igtv"].includes(type)) type = "timeline" + const settings = getSettings(req) - return fetchUser(fill[0]).then(async user => { - const pageNumber = +fill[1] + return fetchUser(username).then(async user => { const pageIndex = pageNumber - 1 - await user.timeline.fetchUpToPage(pageIndex) - if (user.timeline.pages[pageIndex]) { - return render(200, "pug/fragments/timeline_page.pug", {page: user.timeline.pages[pageIndex], pageIndex, user, url, settings}) + const selectedTimeline = user[type] + await selectedTimeline.fetchUpToPage(pageIndex) + if (selectedTimeline.pages[pageIndex]) { + return render(200, "pug/fragments/timeline_page.pug", {page: selectedTimeline.pages[pageIndex], selectedTimeline, type, pageIndex, user, url, settings}) } else { return { statusCode: 400, diff --git a/src/site/html/static/js/pagination.js b/src/site/html/static/js/pagination.js index ce3caf8..c0da4de 100644 --- a/src/site/html/static/js/pagination.js +++ b/src/site/html/static/js/pagination.js @@ -75,8 +75,9 @@ class NextPage extends FreezeWidth { if (this.fetching) return this.fetching = true this.freeze("Loading...") + const type = this.element.getAttribute("data-type") - return fetch(`/fragment/user/${this.element.getAttribute("data-username")}/${this.nextPageNumber}`).then(res => res.text()).then(text => { + return fetch(`/fragment/user/${this.element.getAttribute("data-username")}/${this.nextPageNumber}?type=${type}`).then(res => res.text()).then(text => { q("#next-page-container").remove() this.observer.disconnect() q("#timeline").insertAdjacentHTML("beforeend", text) diff --git a/src/site/pug/fragments/timeline_page.pug b/src/site/pug/fragments/timeline_page.pug index 758a46a..da2f6da 100644 --- a/src/site/pug/fragments/timeline_page.pug +++ b/src/site/pug/fragments/timeline_page.pug @@ -1,6 +1,8 @@ +//- Needs user, selectedTimeline, url, type + include ../includes/timeline_page.pug include ../includes/next_page_button.pug +timeline_page(page, pageIndex) -+next_page_button(user, url) ++next_page_button(user, selectedTimeline, url, type) diff --git a/src/site/pug/includes/next_page_button.pug b/src/site/pug/includes/next_page_button.pug index 9981c00..79190c2 100644 --- a/src/site/pug/includes/next_page_button.pug +++ b/src/site/pug/includes/next_page_button.pug @@ -1,10 +1,10 @@ -mixin next_page_button(user, url) - if user.timeline.hasNextPage() +mixin next_page_button(user, selectedTimeline, url, type) + if selectedTimeline.hasNextPage() div.next-page-container#next-page-container - const nu = new URL(url) - nu.searchParams.set("page", user.timeline.pages.length+1) - a(href=`${nu.search}#page-${user.timeline.pages.length+1}` data-page=(user.timeline.pages.length+1) data-username=(user.data.username))#next-page.next-page Next page + nu.searchParams.set("page", selectedTimeline.pages.length+1) + a(href=`${nu.search}#page-${selectedTimeline.pages.length+1}` data-page=(selectedTimeline.pages.length+1) data-username=(user.data.username) data-type=type)#next-page.next-page Next page else div.page-number.no-more-pages span.number No more posts. diff --git a/src/site/pug/user.pug b/src/site/pug/user.pug index 4c34446..e05bb4b 100644 --- a/src/site/pug/user.pug +++ b/src/site/pug/user.pug @@ -1,4 +1,4 @@ -//- Needs user, followerCountsAvailable, url, constants, settings +//- Needs user, selectedTimeline, type, followerCountsAvailable, url, constants, settings include includes/timeline_page.pug include includes/next_page_button.pug @@ -7,6 +7,9 @@ include includes/feed_link - const numberFormat = new Intl.NumberFormat().format +mixin selector-button(text, selectorType, urlSuffix) + a(href=(type !== selectorType && `/u/${user.data.username}${urlSuffix}`) class=(type === selectorType && "active")).selector= text + doctype html html head @@ -77,17 +80,23 @@ html a(href="/") Home a(href=settingsReferrer) Settings - - const hasPosts = !user.data.is_private && user.timeline.pages.length && user.timeline.pages[0].length - main(class=hasPosts ? "" : "no-posts")#timeline.timeline - if hasPosts - each page, pageIndex in user.timeline.pages - +timeline_page(page, pageIndex) - +next_page_button(user, url) - else - div - div.page-number - span.number - if user.data.is_private - | Profile is private. - else - | No posts. + - const hasPosts = !user.data.is_private && selectedTimeline.pages.length && selectedTimeline.pages[0].length + .timeline-section + .selector-container + +selector-button("Timeline", "timeline", "") + if user.data.has_channel !== false + +selector-button("IGTV", "igtv", "/channel") + + main(class=hasPosts ? "" : "no-posts")#timeline.timeline + if hasPosts + each page, pageIndex in selectedTimeline.pages + +timeline_page(page, pageIndex) + +next_page_button(user, selectedTimeline, url, type) + else + div + div.page-number + span.number + if user.data.is_private + | Profile is private. + else + | No posts. diff --git a/src/site/sass/includes/_main.sass b/src/site/sass/includes/_main.sass index 1a8b8d1..0c85975 100644 --- a/src/site/sass/includes/_main.sass +++ b/src/site/sass/includes/_main.sass @@ -158,25 +158,41 @@ body @media screen and (max-width: $layout-a-max) display: none -.timeline - --image-size: 260px - $image-size: var(--image-size) - - @media screen and (max-width: $layout-a-max) - --image-size: 150px - flex: 1 - - @media screen and (max-width: $layout-c-max) - --image-size: calc(33vw - 10px) - +.timeline-section background-color: map-get($theme, "background-primary") - padding: 15px 15px 40px + padding: 0px 15px 40px - &.no-posts + .selector-container + padding: 15px display: flex - flex-direction: column justify-content: center + .selector + background-color: map-get($theme, "background-primary") + color: map-get($theme, "foreground-primary") + text-decoration: none + padding: 10px 10px 13px + line-height: 1 + font-size: 22px + border: 1px solid transparent + border-bottom: 1px solid map-get($theme, "foreground-timeline-page") + margin: 0px 10px + box-shadow: map-get($theme, "shadow-down-only") + border-radius: 5px + + &:hover, &:focus + border: 1px solid map-get($theme, "foreground-timeline-page") + + &.active + background-color: map-get($theme, "background-power-primary") + color: map-get($theme, "foreground-power-primary") + cursor: default + border: 1px solid map-get($theme, "foreground-timeline-page") + + &:hover, &:focus, &.active + padding-bottom: 10px + border-bottom: 4px solid map-get($theme, "foreground-primary") + .page-number color: map-get($theme, "foreground-timeline-page") line-height: 1 @@ -201,71 +217,87 @@ body padding: 10px background-color: map-get($theme, "background-primary") - .next-page-container - margin: 20px 0px - display: flex - justify-content: center + .timeline + --image-size: 260px + $image-size: var(--image-size) - .next-page - @include link-button - font-size: 18px - text-align: center - - .timeline-inner - display: flex - justify-content: center - flex-wrap: wrap - margin: 0 auto - - &.three-columns - max-width: 810px - - @media screen and (max-width: $layout-a-max) - max-width: 480px - - &.four-columns - max-width: 1080px - - &.six-columns - max-width: 1620px + @media screen and (max-width: $layout-a-max) + --image-size: 150px + flex: 1 @media screen and (max-width: $layout-c-max) - display: grid - grid-template-columns: repeat(3, 1fr) + --image-size: calc(33vw - 10px) + + &.no-posts + display: flex + flex-direction: column justify-content: center - justify-items: center - @mixin sized() - width: $image-size - height: $image-size + .next-page-container + margin: 20px 0px + display: flex + justify-content: center - .sized-link - $margin: 5px + .next-page + @include link-button + font-size: 18px + text-align: center - margin: $margin - color: map-get($theme, "foreground-thumbnail-alt") - border: 0px map-get($theme, "edge-thumbnail-hover") - background-color: map-get($theme, "background-timeline-loading") - text-decoration: none - overflow: hidden - @include sized + .timeline-inner + display: flex + justify-content: center + flex-wrap: wrap + margin: 0 auto - &:hover - $border-width: 3px - margin: $margin - $border-width - border-width: $border-width + &.three-columns + max-width: 810px + + @media screen and (max-width: $layout-a-max) + max-width: 480px + + &.four-columns + max-width: 1080px + + &.six-columns + max-width: 1620px @media screen and (max-width: $layout-c-max) - $margin: 2px + display: grid + grid-template-columns: repeat(3, 1fr) + justify-content: center + justify-items: center + + @mixin sized() + width: $image-size + height: $image-size + + .sized-link + $margin: 5px + margin: $margin + color: map-get($theme, "foreground-thumbnail-alt") + border: 0px map-get($theme, "edge-thumbnail-hover") + background-color: map-get($theme, "background-timeline-loading") + text-decoration: none + overflow: hidden + @include sized &:hover - $border-width: 2px + $border-width: 3px margin: $margin - $border-width border-width: $border-width - .sized-image - @include sized + @media screen and (max-width: $layout-c-max) + $margin: 2px + margin: $margin + + &:hover + $border-width: 2px + margin: $margin - $border-width + border-width: $border-width + + .sized-image + @include sized .post-page background-color: map-get($theme, "background-post-distraction") diff --git a/src/site/sass/themes/_classic.scss b/src/site/sass/themes/_classic.scss index faf1742..f14c55e 100644 --- a/src/site/sass/themes/_classic.scss +++ b/src/site/sass/themes/_classic.scss @@ -54,4 +54,5 @@ $theme: ( "shadow-down": 0px -2px 4px 4px rgba(0, 0, 0, 0.4), "shadow-right": -2px 0px 4px 4px rgba(0, 0, 0, 0.4), "shadow-down-inset": 0px 6px 4px -4px rgba(0, 0, 0, 0.4) inset, + "shadow-down-only": 0px 2px 4px 1px rgba(0, 0, 0, 0.3) );