var HootStream = View.extend({ el: "#hootstream", events: { "click a": "onClickLink", }, initialize: function ({ parent }) { this.parent = parent; this.$hootevents = this.$("#hootevents"); this.hootTemplate = this.$(".hootTemplate").html(); this.threadTemplate = this.$(".threadTemplate").html(); this.lastlogTemplate = this.$(".lastlogTemplate").html(); this.fileTemplate = this.$(".fileTemplate").html(); this.imageTemplate = this.$(".imageTemplate").html(); this.onClickLink = this.onClickLink.bind(this); }, onClickLink: function (event) { if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) { return; } if (!event.target.href) { return; } // console.log(event.target.className, event.target.href); const url = new URL(event.target.href); switch (event.target.className) { case "file": console.log(url.pathname); if (url.pathname.match(/(mp3|wav|ogg|opus|flac)$/i)) { event.preventDefault(); audio.play(event.target.dataset.index); // index set in audio.js } break; case "userLink": case "threadLink": case "keywordLink": case "readMore": event.preventDefault(); console.log(">>", url.pathname); app.router.pushState(url.pathname); app.router.go(url.pathname); break; } // this.parent.onKeyword(keyword) }, load: function (data, filters) { this.state = { ...this.agglutinate(data), data, filters, }; this.build(filters); }, build: function (filters) { const { data, order, threadLookup } = this.state; const threads = Object.values(threadLookup); let sortedOrder; if (filters.sort === "name") { sortedOrder = threads .reduce((names, values) => { if (values.thread.length) { return names.concat([ [values.thread[0].title, values.thread[0].id], ]); } return names; }, []) .sort( filters.order === "asc" ? (a, b) => a[0].localeCompare(b[0]) : (a, b) => b[0].localeCompare(a[0]) ) .map(([, thread_id]) => ({ type: "thread", thread_id, })); } else if (filters.sort === "date") { sortedOrder = filters.order === "desc" ? order : [...order].reverse(); } else { sortedOrder = order; } // console.log(sortedOrder, threadLookup); // Filter the elements that we're going to display const $els = sortedOrder .filter(({ type, thread_id }) => { if (type === "hoot" || type === "lastlog") { return filters.hoots; } const thread = threadLookup[thread_id]; if (filters.hoots && thread.comments && thread.comments.length) { return true; } if ( filters.music && thread.files.some((file) => AUDIO_REGEXP.test(file.filename)) ) { return true; } if ( filters.files && thread.files.some((file) => !AUDIO_REGEXP.test(file.filename)) ) { return true; } if ( (filters.images && thread.images.length) || thread.comments.some((comment) => CONTAINS_IMAGE_URL_REGEXP.test(comment.comment) ) ) { return true; } return false; }) .map( function ({ type, thread_id, data: itemData }) { // console.log(type, thread_id); if (type === "thread") { // Filter thread contents const thread = threadLookup[thread_id]; const threadData = { ...thread, images: filters.images ? thread.images : [], files: filters.music ? thread.files.filter((file) => { if (AUDIO_REGEXP.test(file.filename)) { return filters.music; } return filters.files; }) : [], comments: filters.hoots ? thread.comments : filters.images ? thread.comments.filter((comment) => CONTAINS_IMAGE_URL_REGEXP.test(comment.comment) ) : [], query: data.query, }; if ( threadData.images.length || threadData.files.length || threadData.comments.length ) { return this.renderThread(threadData).reduce( ($el, $item) => $el.append($item), $("
") ); } else { return $(); } } return type === "hoot" ? this.renderHoot(itemData) : type === "lastlog" ? this.renderLastlog(itemData) : "Unknown item"; }.bind(this) ); this.$hootevents.empty(); this.$hootevents.append($els); audio.init(); }, /** Render Methods */ render: (template, object) => { if (!template) { console.error("No template", object); return $("
No template
"); } const rendered = Object.entries(object).reduce( (newTemplate, [key, value]) => newTemplate.replace(new RegExp(`{{${key}}}`, "g"), value), template ); return $(rendered); }, renderLastlog: function ({ username, lastseen }) { const age = get_age(lastseen); const age_ago = age === "now" ? age : `${age} ago`; return this.render(this.lastlogTemplate, { className: "hoot streamLastlog", username, age, opacity: 0.6, showAvatar: 0, hoot: "last seen " + age_ago, }); }, renderHoot: function ({ id, thread, date, username, hoot, comment, hidden, className, showAvatar, template, ...options }) { // console.log(hoot, comment); const age_opacity = get_age_opacity(date); return this.render(template || this.hootTemplate, { username, className: className ? `hoot ${className}` : "hoot", image: profile_image(username), showAvatar: showAvatar === false ? 0 : 1, hoot: hoot || "
" + tidy_urls(comment, true) + "
", age: get_age(date), hoot_opacity: className === "first_post" ? 1.0 : age_opacity, age_opacity: age_opacity, ...options, }); }, renderHoots: function ({ hoots, className }) { const els = []; for (hoot of hoots) { els.push(this.renderHoot({ ...hoot, className })); } return els; }, renderThread: function ({ query, thread, comments, files, images }) { if (!thread || !thread.length) { console.error("Missing thread"); console.error(thread, comments, files, images); return ["
Missing thread!
"]; } thread = thread[0]; const isViewingKeyword = query.keyword === thread.keyword; const isViewingThread = query.thread === thread.id; // console.log(thread, comments, files, images); const postedToday = +new Date() / 1000 - thread.lastmodified < 86400; const age_opacity = get_age_opacity(thread.lastmodified); const thread_opacity = clamp( get_age_opacity(thread.lastmodified) + 0.2, 0.0, 1.0 ); const filteredImages = images .map((image) => [image, new RegExp(encodeURIComponent(image.filename))]) .filter( ([, filenameRegexp]) => !comments.some((comment) => filenameRegexp.test(comment.comment)) ) .map(([image]) => image); // console.log(filteredImages); // console.log(thread); return [ "
", this.renderHoot({ template: this.threadTemplate, hoot: `${thread.title}`, thread_opacity, keyword_link: thread.keyword ? `${thread.keyword}` : "", username: thread.username, className: postedToday ? "isRecent" : "", date: thread.lastmodified, file_count: `${thread.file_count || 0} f.`, file_opacity: age_opacity * get_size_opacity(thread.file_count), comment_count: `${thread.comment_count || 0} c.`, comment_opacity: age_opacity * get_size_opacity(thread.comment_count), }), this.renderImages( isViewingThread || postedToday ? filteredImages : filteredImages.slice(0, 6) ), this.renderFiles( isViewingThread || postedToday ? files : files.slice(0, 10) ), ...this.renderHoots({ hoots: comments.slice(0, 1).map( trimComment({ isViewingThread, lines: 5, snippetSize: 512, cropSize: 256, }) ), className: "first_post", }), ...this.renderHoots({ hoots: isViewingThread || postedToday ? comments.slice(1).map( trimComment({ isViewingThread, lines: 1, snippetSize: 256, cropSize: 128, }) ) : comments .slice(1) .slice(-5) .map( trimComment({ isViewingThread, lines: 1, snippetSize: 256, cropSize: 128, }) ), }), "
", ]; }, renderImages: function (images) { if (!images.length) { return null; } const $table = $("
"); for (const image of images) { const $el = this.renderFile(this.imageTemplate, image); $table.append($el); } return $table; }, renderFiles: function (files) { if (!files.length) { return null; } const $table = $("
"); for (const file of files.sort((file) => file.filename.toLowerCase())) { const $el = this.renderFile(this.fileTemplate, file); $table.append($el); } return $table; }, renderFile: function (template, file) { var size = hush_size(file.size); var datetime = verbose_date(file.date, true); var date_class = carbon_date(file.date); var link = make_link(file); return this.render(template, { id: file.id, username: file.username, link, filename: file.filename, age: get_age(file.date), age_opacity: get_age_opacity(file.date), date_class, date: datetime[0], // time: datetime[1], // size_class: size[0], size: size[1], }); }, agglutinate: ({ query, threads, comments, files, hootbox, lastlog }) => [ ...threads.map((thread) => [ thread.id, thread.createdate, "thread", thread, ]), ...comments .filter((comment) => comment.thread !== 1) .map((comment) => [comment.thread, comment.date, "comments", comment]), ...files.map((file) => [ file.thread, file.date, IMAGE_REGEXP.test(file.filename) ? "images" : "files", file, ]), ...(query.has_query ? [] : hootbox).map((hoot) => [ 1, hoot.date, "hoot", hoot, ]), ...(query.has_query ? [] : lastlog).map((user) => [ 1, user.lastseen, "lastlog", user, ]), ] .sort((a, b) => b[1] - a[1]) .reduce( ({ threadLookup, order }, [thread_id, date, type, data]) => { if (type === "hoot") { order.push({ type: "hoot", date, data }); } else if (type === "lastlog") { order.push({ type: "lastlog", date, data }); } else if (thread_id !== 1) { if (!(thread_id in threadLookup)) { threadLookup[thread_id] = { thread: [], comments: [], files: [], images: [], }; order.push({ type: "thread", date, thread_id }); } threadLookup[thread_id][type].push(data); } return { threadLookup, order }; }, { threadLookup: {}, order: [] } ), });