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: [] }
),
});