Rejigger timelines to avoid explicit types
This commit is contained in:
parent
6176744462
commit
09e6a15085
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -eou pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
# This script is for use in local development only. It wipes the existing db,
|
# This script is for use in local development only. It wipes the existing db,
|
||||||
# creates a new empty one, runs the initial migration to create the schema,
|
# creates a new empty one, runs the initial migration to create the schema,
|
||||||
|
@ -9,8 +9,8 @@ set -eou pipefail
|
||||||
# TODO(opensource): We should adapt Asaf's seedfile command and then delete this.
|
# TODO(opensource): We should adapt Asaf's seedfile command and then delete this.
|
||||||
|
|
||||||
THIS_PATH=$(pwd)
|
THIS_PATH=$(pwd)
|
||||||
BETA_PATH='/mnt/c/Users/bvisn/Developer/handmade/handmade-beta'
|
#BETA_PATH='/mnt/c/Users/bvisn/Developer/handmade/handmade-beta'
|
||||||
# BETA_PATH='/Users/benvisness/Developer/handmade/handmade-beta'
|
BETA_PATH='/Users/benvisness/Developer/handmade/handmade-beta'
|
||||||
|
|
||||||
pushd $BETA_PATH
|
pushd $BETA_PATH
|
||||||
docker-compose down -v
|
docker-compose down -v
|
||||||
|
@ -19,4 +19,4 @@ pushd $BETA_PATH
|
||||||
|
|
||||||
docker-compose exec postgres bash -c "psql -U postgres -c \"CREATE ROLE hmn CREATEDB LOGIN PASSWORD 'password';\""
|
docker-compose exec postgres bash -c "psql -U postgres -c \"CREATE ROLE hmn CREATEDB LOGIN PASSWORD 'password';\""
|
||||||
popd
|
popd
|
||||||
go run src/main.go seedfile local/backups/hmn_pg_dump_live_2021-09-06
|
go run src/main.go seedfile local/backups/hmn_pg_dump_live_2021-10-23
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
const TimelineTypes = {
|
const TimelineMediaTypes = {
|
||||||
UNKNOWN: 0,
|
IMAGE: 1,
|
||||||
FORUM_THREAD: 1,
|
VIDEO: 2,
|
||||||
FORUM_REPLY: 2,
|
AUDIO: 3,
|
||||||
BLOG_POST: 3,
|
EMBED: 4,
|
||||||
BLOG_COMMENT: 4,
|
}
|
||||||
SNIPPET_IMAGE: 5,
|
|
||||||
SNIPPET_VIDEO: 6,
|
|
||||||
SNIPPET_AUDIO: 7,
|
|
||||||
SNIPPET_YOUTUBE: 8
|
|
||||||
};
|
|
||||||
|
|
||||||
const showcaseItemTemplate = makeTemplateCloner("showcase_item");
|
const showcaseItemTemplate = makeTemplateCloner("showcase_item");
|
||||||
const modalTemplate = makeTemplateCloner("timeline_modal");
|
const modalTemplate = makeTemplateCloner("timeline_modal");
|
||||||
|
@ -39,10 +34,10 @@ function makeShowcaseItem(timelineItem) {
|
||||||
let addThumbnailFunc = () => {};
|
let addThumbnailFunc = () => {};
|
||||||
let createModalContentFunc = () => {};
|
let createModalContentFunc = () => {};
|
||||||
|
|
||||||
switch (timelineItem.type) {
|
switch (timelineItem.media_type) {
|
||||||
case TimelineTypes.SNIPPET_IMAGE:
|
case TimelineMediaTypes.IMAGE:
|
||||||
addThumbnailFunc = () => {
|
addThumbnailFunc = () => {
|
||||||
itemEl.thumbnail.style.backgroundImage = `url('${timelineItem.asset_url}')`;
|
itemEl.thumbnail.style.backgroundImage = `url('${timelineItem.thumbnail_url}')`;
|
||||||
};
|
};
|
||||||
|
|
||||||
createModalContentFunc = () => {
|
createModalContentFunc = () => {
|
||||||
|
@ -53,10 +48,10 @@ function makeShowcaseItem(timelineItem) {
|
||||||
};
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case TimelineTypes.SNIPPET_VIDEO:
|
case TimelineMediaTypes.VIDEO:
|
||||||
addThumbnailFunc = () => {
|
addThumbnailFunc = () => {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.src = timelineItem.asset_url;
|
video.src = timelineItem.asset_url; // TODO: Use image thumbnails
|
||||||
video.controls = false;
|
video.controls = false;
|
||||||
video.classList.add('h-100');
|
video.classList.add('h-100');
|
||||||
video.preload = 'metadata';
|
video.preload = 'metadata';
|
||||||
|
@ -73,7 +68,7 @@ function makeShowcaseItem(timelineItem) {
|
||||||
};
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case TimelineTypes.SNIPPET_AUDIO:
|
case TimelineMediaTypes.AUDIO:
|
||||||
createModalContentFunc = () => {
|
createModalContentFunc = () => {
|
||||||
const modalAudio = document.createElement('audio');
|
const modalAudio = document.createElement('audio');
|
||||||
modalAudio.src = timelineItem.asset_url;
|
modalAudio.src = timelineItem.asset_url;
|
||||||
|
|
|
@ -9364,12 +9364,12 @@ span.icon-rss::before {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 80vh; }
|
max-height: 80vh; }
|
||||||
.timeline-item .timeline-content-box.youtube {
|
.timeline-item .timeline-content-box.embed {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
padding-bottom: 56.25%; }
|
padding-bottom: 56.25%; }
|
||||||
.timeline-item .timeline-content-box.youtube > iframe {
|
.timeline-item .timeline-content-box.embed > iframe {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -1,17 +1,4 @@
|
||||||
.timeline {
|
.timeline {
|
||||||
&.no-forums .forums {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.no-blogs .blogs {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.no-library .library {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.no-snippets .snippets {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item {
|
.timeline-item {
|
||||||
@include usevar(background-color, card-background);
|
@include usevar(background-color, card-background);
|
||||||
@include usevar(color, main-color);
|
@include usevar(color, main-color);
|
||||||
|
@ -41,7 +28,7 @@
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-content-box.youtube {
|
.timeline-content-box.embed {
|
||||||
// NOTE(asaf): CSS trick to get an iframe to auto resize
|
// NOTE(asaf): CSS trick to get an iframe to auto resize
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -254,10 +254,6 @@ func TimelineItemsToJSON(items []TimelineItem) string {
|
||||||
}
|
}
|
||||||
builder.WriteRune('{')
|
builder.WriteRune('{')
|
||||||
|
|
||||||
builder.WriteString(`"type":`)
|
|
||||||
builder.WriteString(strconv.Itoa(int(item.Type)))
|
|
||||||
builder.WriteRune(',')
|
|
||||||
|
|
||||||
builder.WriteString(`"date":`)
|
builder.WriteString(`"date":`)
|
||||||
builder.WriteString(strconv.FormatInt(item.Date.UTC().Unix(), 10))
|
builder.WriteString(strconv.FormatInt(item.Date.UTC().Unix(), 10))
|
||||||
builder.WriteRune(',')
|
builder.WriteRune(',')
|
||||||
|
@ -288,16 +284,36 @@ func TimelineItemsToJSON(items []TimelineItem) string {
|
||||||
builder.WriteString(item.Url)
|
builder.WriteString(item.Url)
|
||||||
builder.WriteString(`",`)
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
|
var mediaType TimelineItemMediaType
|
||||||
|
var assetUrl string
|
||||||
|
var thumbnailUrl string
|
||||||
|
var width, height int
|
||||||
|
if len(item.EmbedMedia) > 0 {
|
||||||
|
mediaType = item.EmbedMedia[0].Type
|
||||||
|
assetUrl = item.EmbedMedia[0].AssetUrl
|
||||||
|
thumbnailUrl = item.EmbedMedia[0].ThumbnailUrl
|
||||||
|
width = item.EmbedMedia[0].Width
|
||||||
|
height = item.EmbedMedia[0].Height
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString(`"media_type":`)
|
||||||
|
builder.WriteString(strconv.Itoa(int(mediaType)))
|
||||||
|
builder.WriteRune(',')
|
||||||
|
|
||||||
builder.WriteString(`"width":`)
|
builder.WriteString(`"width":`)
|
||||||
builder.WriteString(strconv.Itoa(item.Width))
|
builder.WriteString(strconv.Itoa(width))
|
||||||
builder.WriteRune(',')
|
builder.WriteRune(',')
|
||||||
|
|
||||||
builder.WriteString(`"height":`)
|
builder.WriteString(`"height":`)
|
||||||
builder.WriteString(strconv.Itoa(item.Height))
|
builder.WriteString(strconv.Itoa(height))
|
||||||
builder.WriteRune(',')
|
builder.WriteRune(',')
|
||||||
|
|
||||||
builder.WriteString(`"asset_url":"`)
|
builder.WriteString(`"asset_url":"`)
|
||||||
builder.WriteString(item.AssetUrl)
|
builder.WriteString(assetUrl)
|
||||||
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
|
builder.WriteString(`"thumbnail_url":"`)
|
||||||
|
builder.WriteString(thumbnailUrl)
|
||||||
builder.WriteString(`",`)
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
builder.WriteString(`"discord_message_url":"`)
|
builder.WriteString(`"discord_message_url":"`)
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<entry>
|
<entry>
|
||||||
<title>New showcase item by {{ .OwnerName }}</title>
|
<title>New showcase item by {{ .OwnerName }}</title>
|
||||||
<link rel="alternative" type="text/html" href="{{ .Url }}" />
|
<link rel="alternative" type="text/html" href="{{ .Url }}" />
|
||||||
<id>{{ .UUID }}</id>
|
<id>{{ string2uuid .Url }}</id>
|
||||||
<published>{{ rfc3339 .Date }}</published>
|
<published>{{ rfc3339 .Date }}</published>
|
||||||
<author>
|
<author>
|
||||||
<name>{{ .OwnerName }}</name>
|
<name>{{ .OwnerName }}</name>
|
||||||
|
@ -54,13 +54,19 @@
|
||||||
</author>
|
</author>
|
||||||
<content type="xhtml">
|
<content type="xhtml">
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml">
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||||
{{ .Description }}
|
<div>
|
||||||
{{ if snippetimage . }}
|
{{ .Description }}
|
||||||
<img src="{{ .AssetUrl }}"/>
|
</div>
|
||||||
{{ else if snippetvideo . }}
|
{{ range .EmbedMedia }}
|
||||||
<video src="{{ .AssetUrl }}" controls="true"/>
|
<div>
|
||||||
{{ else if snippetaudio . }}
|
{{ if eq .Type mediaimage }}
|
||||||
<audio src="{{ .AssetUrl }}" controls="true"/>
|
<img src="{{ .AssetUrl }}"/>
|
||||||
|
{{ else if eq .Type mediavideo }}
|
||||||
|
<video src="{{ .AssetUrl }}" controls="true"/>
|
||||||
|
{{ else if eq .Type mediaaudio }}
|
||||||
|
<audio src="{{ .AssetUrl }}" controls="true"/>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</content>
|
</content>
|
||||||
|
|
|
@ -1,5 +1,31 @@
|
||||||
{{ if timelinepostitem . }}
|
{{/*
|
||||||
<div class="timeline-item flex pa3 mb2 br3 {{ .Class }}">
|
TODO: The logic in here for how to present various types of timeline items needs to be more robust.
|
||||||
|
*/}}
|
||||||
|
|
||||||
|
{{ if or .Description .EmbedMedia }}
|
||||||
|
<div class="timeline-item flex flex-column pa3 mb2 br3" data-filter-title="{{ .FilterTitle }}">
|
||||||
|
<div class="timeline-user-info mb2 flex items-center">
|
||||||
|
<img class="avatar-icon lite mr2" src="{{ .OwnerAvatarUrl }}"/>
|
||||||
|
<a class="user" href="{{ .OwnerUrl }}">{{ .OwnerName }}</a>
|
||||||
|
<a class="datetime tr" style="flex: 1 1 auto;" href="{{ .Url }}">{{ timehtml (relativedate .Date) .Date }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="mb2">{{ .Description }}</div>
|
||||||
|
{{ range .EmbedMedia }}
|
||||||
|
<div class="timeline-content-box {{ if eq .Type mediaembed }}embed{{ end }}">
|
||||||
|
{{ if eq .Type mediaimage }}
|
||||||
|
<img src="{{ .AssetUrl }}">
|
||||||
|
{{ else if eq .Type mediavideo }}
|
||||||
|
<video src="{{ .AssetUrl }}" preload="metadata" controls>
|
||||||
|
{{ else if eq .Type mediaaudio }}
|
||||||
|
<audio src="{{ .AssetUrl }}" controls>
|
||||||
|
{{ else if eq .Type mediaembed }}
|
||||||
|
{{ .EmbedHTML }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ else }}
|
||||||
|
<div class="timeline-item flex pa3 mb2 br3" data-filter-title="{{ .FilterTitle }}">
|
||||||
<img class="avatar-icon big lite mr3" src="{{ .OwnerAvatarUrl }}"/>
|
<img class="avatar-icon big lite mr3" src="{{ .OwnerAvatarUrl }}"/>
|
||||||
<div class="timeline-info overflow-hidden">
|
<div class="timeline-info overflow-hidden">
|
||||||
{{ template "breadcrumbs.html" .Breadcrumbs }}
|
{{ template "breadcrumbs.html" .Breadcrumbs }}
|
||||||
|
@ -9,25 +35,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ else if timelinesnippetitem . }}
|
|
||||||
<div class="timeline-item flex flex-column pa3 mb2 br3 {{ .Class }}">
|
|
||||||
<div class="timeline-user-info mb2 flex items-center">
|
|
||||||
<img class="avatar-icon lite mr2" src="{{ .OwnerAvatarUrl }}"/>
|
|
||||||
<a class="user" href="{{ .OwnerUrl }}">{{ .OwnerName }}</a>
|
|
||||||
<a class="datetime tr" style="flex: 1 1 auto;" href="{{ .Url }}">{{ timehtml (relativedate .Date) .Date }}</a>
|
|
||||||
</div>
|
|
||||||
<p class="timeline-snippet-title mb2">{{ .Description }}</p>
|
|
||||||
<div class="timeline-content-box {{ if snippetyoutube . }}youtube{{ end }}">
|
|
||||||
{{ if snippetvideo . }}
|
|
||||||
<video src="{{ .AssetUrl }}" preload="metadata" controls />
|
|
||||||
{{ else if snippetimage . }}
|
|
||||||
<img src="{{ .AssetUrl }}" />
|
|
||||||
{{ else if snippetaudio . }}
|
|
||||||
<audio src="{{ .AssetUrl }}" controls />
|
|
||||||
{{ else if snippetyoutube .}}
|
|
||||||
<iframe src="https://www.youtube-nocookie.com/embed/{{ .YoutubeID }}" allow="accelerometer; encrypted-media; gyroscope;" allowfullscreen frameborder="0"></iframe>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
|
@ -10,17 +10,19 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="mb2">{{ .Snippet.Description }}</p>
|
<p class="mb2">{{ .Snippet.Description }}</p>
|
||||||
<div>
|
<div>
|
||||||
{{ if snippetimage .Snippet }}
|
{{ range .Snippet.EmbedMedia }}
|
||||||
<img src="{{ .Snippet.AssetUrl }}" />
|
{{ if eq .Type mediaimage }}
|
||||||
{{ else if snippetvideo .Snippet }}
|
<img src="{{ .AssetUrl }}">
|
||||||
<video src="{{ .Snippet.AssetUrl }}" preload="metadata" controls />
|
{{ else if eq .Type mediavideo }}
|
||||||
{{ else if snippetaudio .Snippet }}
|
<video src="{{ .AssetUrl }}" preload="metadata" controls>
|
||||||
<audio src="{{ .Snippet.AssetUrl }}" controls />
|
{{ else if eq .Type mediaaudio }}
|
||||||
{{ else if snippetyoutube .Snippet }}
|
<audio src="{{ .AssetUrl }}" controls>
|
||||||
<div class="mb3 aspect-ratio aspect-ratio--16x9">
|
{{ else if eq .Type mediaembed }}
|
||||||
<iframe class="aspect-ratio--object" src="https://www.youtube-nocookie.com/embed/{{ .Snippet.YoutubeID }}" allow="accelerometer; encrypted-media; gyroscope;" allowfullscreen frameborder="0" />
|
<div class="mb3 aspect-ratio aspect-ratio--16x9">
|
||||||
</div>
|
{{ .EmbedHTML }}
|
||||||
{{ end }}
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,19 +6,18 @@
|
||||||
{{ with .ProfileUserProjects }}
|
{{ with .ProfileUserProjects }}
|
||||||
<div class="content-block ph3 ph0-ns">
|
<div class="content-block ph3 ph0-ns">
|
||||||
<h2>Projects</h2>
|
<h2>Projects</h2>
|
||||||
<div class="ph3">
|
{{ range . }}
|
||||||
{{ range . }}
|
<div class="mv3">
|
||||||
<div class="mv3">
|
{{ template "project_card.html" projectcarddata . "" }}
|
||||||
{{ template "project_card.html" projectcarddata . "" }}
|
</div>
|
||||||
</div>
|
{{ end }}
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if gt (len .TimelineItems) 0 }}
|
{{ if gt (len .TimelineItems) 0 }}
|
||||||
<div class="content-block timeline-container ph3 ph0-ns">
|
<div class="content-block timeline-container ph3 ph0-ns">
|
||||||
<h2>Recent Activity</h2>
|
<h2>Recent Activity</h2>
|
||||||
<div class="timeline-filters mb2">
|
<div class="timeline-filters mb2">
|
||||||
|
{{/*
|
||||||
{{ if gt .NumForums 0 }}
|
{{ if gt .NumForums 0 }}
|
||||||
<div class="dib filter forums mr2"><input data-type="forums" class="v-mid mr1" type="checkbox" id="timeline-checkbox-forums" checked /><label class="v-mid" for="timeline-checkbox-forums">Forums (<span class="count">{{ .NumForums }}</span>)</label></div>
|
<div class="dib filter forums mr2"><input data-type="forums" class="v-mid mr1" type="checkbox" id="timeline-checkbox-forums" checked /><label class="v-mid" for="timeline-checkbox-forums">Forums (<span class="count">{{ .NumForums }}</span>)</label></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -28,6 +27,7 @@
|
||||||
{{ if gt .NumSnippets 0 }}
|
{{ if gt .NumSnippets 0 }}
|
||||||
<div class="dib filter snippets mr2"><input data-type="snippets" class="v-mid mr1" type="checkbox" id="timeline-checkbox-snippets" checked /><label class="v-mid" for="timeline-checkbox-snippets">Snippets (<span class="count">{{ .NumSnippets }}</span>)</label></div>
|
<div class="dib filter snippets mr2"><input data-type="snippets" class="v-mid mr1" type="checkbox" id="timeline-checkbox-snippets" checked /><label class="v-mid" for="timeline-checkbox-snippets">Snippets (<span class="count">{{ .NumSnippets }}</span>)</label></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
*/}}
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
{{ range .TimelineItems }}
|
{{ range .TimelineItems }}
|
||||||
|
@ -53,11 +53,6 @@
|
||||||
<div class="value">{{ absoluteshortdate .ProfileUser.DateJoined }}</div>
|
<div class="value">{{ absoluteshortdate .ProfileUser.DateJoined }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pair flex flex-wrap">
|
|
||||||
<div class="key flex-auto mr1">Posts</div>
|
|
||||||
<div class="value">{{ add .NumForums .NumBlogs }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ if .ProfileUser.Email }}
|
{{ if .ProfileUser.Email }}
|
||||||
<div class="pair flex flex-wrap">
|
<div class="pair flex flex-wrap">
|
||||||
<div class="key flex-auto mr1">Email</div>
|
<div class="key flex-auto mr1">Email</div>
|
||||||
|
@ -75,15 +70,50 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
let timelineList = document.querySelector(".timeline");
|
const filterTitles = [];
|
||||||
let toggles = document.querySelectorAll(".timeline-filters .filter input");
|
for (const item of document.querySelectorAll('.timeline-item')) {
|
||||||
function timelineFilterToggle(ev) {
|
const title = item.getAttribute('data-filter-title');
|
||||||
timelineList.classList.toggle("no-" + ev.target.getAttribute("data-type"), !ev.target.checked);
|
if (title && !filterTitles.includes(title)) {
|
||||||
|
filterTitles.push(title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < toggles.length; ++i) {
|
filterTitles.sort();
|
||||||
let toggle = toggles[i];
|
|
||||||
toggle.checked = true;
|
function itemsForFilterTitle(title) {
|
||||||
toggle.addEventListener("change", timelineFilterToggle);
|
return document.querySelectorAll(`.timeline-item[data-filter-title="${title}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = document.querySelector('.timeline-filters');
|
||||||
|
|
||||||
|
for (const title of filterTitles) {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.className = "dib filter mr2";
|
||||||
|
|
||||||
|
const id = `timeline-checkbox-${title.replaceAll(/\s/g, '-')}`;
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.className = "v-mid mr1";
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.id = id;
|
||||||
|
input.checked = true;
|
||||||
|
input.addEventListener("change", e => {
|
||||||
|
for (const item of itemsForFilterTitle(title)) {
|
||||||
|
if (e.target.checked) {
|
||||||
|
item.style.removeProperty("display");
|
||||||
|
} else {
|
||||||
|
item.style.setProperty("display", "none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
container.appendChild(input);
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.className = "v-mid";
|
||||||
|
label.htmlFor = id;
|
||||||
|
label.innerText = `${title} (${itemsForFilterTitle(title).length})`;
|
||||||
|
container.appendChild(label);
|
||||||
|
|
||||||
|
filters.appendChild(container);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
"git.handmade.network/hmn/hmn/src/logging"
|
"git.handmade.network/hmn/hmn/src/logging"
|
||||||
"github.com/Masterminds/sprig"
|
"github.com/Masterminds/sprig"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/teacat/noire"
|
"github.com/teacat/noire"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -179,6 +180,9 @@ var HMNTemplateFuncs = template.FuncMap{
|
||||||
"staticthemenobust": func(theme string, filepath string) string {
|
"staticthemenobust": func(theme string, filepath string) string {
|
||||||
return hmnurl.BuildTheme(filepath, theme, false)
|
return hmnurl.BuildTheme(filepath, theme, false)
|
||||||
},
|
},
|
||||||
|
"string2uuid": func(s string) string {
|
||||||
|
return uuid.NewSHA1(uuid.NameSpaceURL, []byte(s)).URN()
|
||||||
|
},
|
||||||
"timehtml": func(formatted string, t time.Time) template.HTML {
|
"timehtml": func(formatted string, t time.Time) template.HTML {
|
||||||
iso := t.UTC().Format(time.RFC3339)
|
iso := t.UTC().Format(time.RFC3339)
|
||||||
return template.HTML(fmt.Sprintf(`<time datetime="%s">%s</time>`, iso, formatted))
|
return template.HTML(fmt.Sprintf(`<time datetime="%s">%s</time>`, iso, formatted))
|
||||||
|
@ -195,36 +199,8 @@ var HMNTemplateFuncs = template.FuncMap{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"timelinepostitem": func(item TimelineItem) bool {
|
"mediaimage": func() TimelineItemMediaType { return TimelineItemMediaTypeImage },
|
||||||
if item.Type == TimelineTypeForumThread ||
|
"mediavideo": func() TimelineItemMediaType { return TimelineItemMediaTypeVideo },
|
||||||
item.Type == TimelineTypeForumReply ||
|
"mediaaudio": func() TimelineItemMediaType { return TimelineItemMediaTypeAudio },
|
||||||
item.Type == TimelineTypeBlogPost ||
|
"mediaembed": func() TimelineItemMediaType { return TimelineItemMediaTypeEmbed },
|
||||||
item.Type == TimelineTypeBlogComment {
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
"timelinesnippetitem": func(item TimelineItem) bool {
|
|
||||||
if item.Type == TimelineTypeSnippetImage ||
|
|
||||||
item.Type == TimelineTypeSnippetVideo ||
|
|
||||||
item.Type == TimelineTypeSnippetAudio ||
|
|
||||||
item.Type == TimelineTypeSnippetYoutube {
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
"snippetvideo": func(snippet TimelineItem) bool {
|
|
||||||
return snippet.Type == TimelineTypeSnippetVideo
|
|
||||||
},
|
|
||||||
"snippetaudio": func(snippet TimelineItem) bool {
|
|
||||||
return snippet.Type == TimelineTypeSnippetAudio
|
|
||||||
},
|
|
||||||
"snippetimage": func(snippet TimelineItem) bool {
|
|
||||||
return snippet.Type == TimelineTypeSnippetImage
|
|
||||||
},
|
|
||||||
"snippetyoutube": func(snippet TimelineItem) bool {
|
|
||||||
return snippet.Type == TimelineTypeSnippetYoutube
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -244,45 +244,45 @@ type ThreadListItem struct {
|
||||||
Content string
|
Content string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimelineType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TimelineTypeUnknown TimelineType = iota
|
|
||||||
|
|
||||||
TimelineTypeForumThread
|
|
||||||
TimelineTypeForumReply
|
|
||||||
|
|
||||||
TimelineTypeBlogPost
|
|
||||||
TimelineTypeBlogComment
|
|
||||||
|
|
||||||
TimelineTypeSnippetImage
|
|
||||||
TimelineTypeSnippetVideo
|
|
||||||
TimelineTypeSnippetAudio
|
|
||||||
TimelineTypeSnippetYoutube
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimelineItem struct {
|
type TimelineItem struct {
|
||||||
Type TimelineType
|
Date time.Time
|
||||||
TypeTitle string
|
Title string
|
||||||
Class string
|
TypeTitle string
|
||||||
Date time.Time
|
FilterTitle string
|
||||||
Url string
|
Breadcrumbs []Breadcrumb
|
||||||
UUID string
|
Url string
|
||||||
|
DiscordMessageUrl string
|
||||||
|
|
||||||
OwnerAvatarUrl string
|
OwnerAvatarUrl string
|
||||||
OwnerName string
|
OwnerName string
|
||||||
OwnerUrl string
|
OwnerUrl string
|
||||||
Description template.HTML
|
|
||||||
|
|
||||||
DiscordMessageUrl string
|
Description template.HTML
|
||||||
Width int
|
TruncateDescription bool
|
||||||
Height int
|
|
||||||
AssetUrl string
|
|
||||||
MimeType string
|
|
||||||
YoutubeID string
|
|
||||||
|
|
||||||
Title string
|
PreviewMedia TimelineItemMedia
|
||||||
Breadcrumbs []Breadcrumb
|
EmbedMedia []TimelineItemMedia
|
||||||
|
|
||||||
|
CanShowcase bool // whether this snippet can be shown in a showcase gallery
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimelineItemMediaType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TimelineItemMediaTypeImage TimelineItemMediaType = iota + 1
|
||||||
|
TimelineItemMediaTypeVideo
|
||||||
|
TimelineItemMediaTypeAudio
|
||||||
|
TimelineItemMediaTypeEmbed
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimelineItemMedia struct {
|
||||||
|
Type TimelineItemMediaType
|
||||||
|
AssetUrl string
|
||||||
|
EmbedHTML template.HTML
|
||||||
|
ThumbnailUrl string
|
||||||
|
MimeType string
|
||||||
|
Width, Height int
|
||||||
|
ExtraOpenGraphItems []OpenGraphItem
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectCardData struct {
|
type ProjectCardData struct {
|
||||||
|
|
|
@ -252,7 +252,6 @@ func AtomFeed(c *RequestContext) ResponseData {
|
||||||
for _, s := range snippetQuerySlice {
|
for _, s := range snippetQuerySlice {
|
||||||
row := s.(*snippetQuery)
|
row := s.(*snippetQuery)
|
||||||
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
||||||
timelineItem.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(timelineItem.Url)).URN()
|
|
||||||
feedData.Snippets = append(feedData.Snippets, timelineItem)
|
feedData.Snippets = append(feedData.Snippets, timelineItem)
|
||||||
}
|
}
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
|
@ -49,7 +49,7 @@ func JamIndex(c *RequestContext) ResponseData {
|
||||||
for _, s := range snippetQuerySlice {
|
for _, s := range snippetQuerySlice {
|
||||||
row := s.(*snippetQuery)
|
row := s.(*snippetQuery)
|
||||||
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
||||||
if timelineItem.Type != templates.TimelineTypeSnippetYoutube {
|
if timelineItem.CanShowcase {
|
||||||
showcaseItems = append(showcaseItems, timelineItem)
|
showcaseItems = append(showcaseItems, timelineItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,7 +246,7 @@ func Index(c *RequestContext) ResponseData {
|
||||||
for _, s := range snippetQuerySlice {
|
for _, s := range snippetQuerySlice {
|
||||||
row := s.(*snippetQuery)
|
row := s.(*snippetQuery)
|
||||||
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
||||||
if timelineItem.Type != templates.TimelineTypeSnippetYoutube {
|
if timelineItem.CanShowcase {
|
||||||
showcaseItems = append(showcaseItems, timelineItem)
|
showcaseItems = append(showcaseItems, timelineItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ func Showcase(c *RequestContext) ResponseData {
|
||||||
for _, s := range snippetQuerySlice {
|
for _, s := range snippetQuerySlice {
|
||||||
row := s.(*snippetQuery)
|
row := s.(*snippetQuery)
|
||||||
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
||||||
if timelineItem.Type != templates.TimelineTypeSnippetYoutube {
|
if timelineItem.CanShowcase {
|
||||||
showcaseItems = append(showcaseItems, timelineItem)
|
showcaseItems = append(showcaseItems, timelineItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,37 +70,34 @@ func Snippet(c *RequestContext) ResponseData {
|
||||||
{Property: "og:description", Value: string(snippet.Description)},
|
{Property: "og:description", Value: string(snippet.Description)},
|
||||||
}
|
}
|
||||||
|
|
||||||
if snippet.Type == templates.TimelineTypeSnippetImage {
|
if len(snippet.EmbedMedia) > 0 {
|
||||||
opengraphImage := []templates.OpenGraphItem{
|
media := snippet.EmbedMedia[0]
|
||||||
{Property: "og:image", Value: snippet.AssetUrl},
|
|
||||||
{Property: "og:image:width", Value: strconv.Itoa(snippet.Width)},
|
switch media.Type {
|
||||||
{Property: "og:image:height", Value: strconv.Itoa(snippet.Height)},
|
case templates.TimelineItemMediaTypeImage:
|
||||||
{Property: "og:image:type", Value: snippet.MimeType},
|
opengraph = append(opengraph,
|
||||||
{Name: "twitter:card", Value: "summary_large_image"},
|
templates.OpenGraphItem{Property: "og:image", Value: media.AssetUrl},
|
||||||
|
templates.OpenGraphItem{Property: "og:image:width", Value: strconv.Itoa(media.Width)},
|
||||||
|
templates.OpenGraphItem{Property: "og:image:height", Value: strconv.Itoa(media.Height)},
|
||||||
|
templates.OpenGraphItem{Property: "og:image:type", Value: media.MimeType},
|
||||||
|
templates.OpenGraphItem{Name: "twitter:card", Value: "summary_large_image"},
|
||||||
|
)
|
||||||
|
case templates.TimelineItemMediaTypeVideo:
|
||||||
|
opengraph = append(opengraph,
|
||||||
|
templates.OpenGraphItem{Property: "og:video", Value: media.AssetUrl},
|
||||||
|
templates.OpenGraphItem{Property: "og:video:width", Value: strconv.Itoa(media.Width)},
|
||||||
|
templates.OpenGraphItem{Property: "og:video:height", Value: strconv.Itoa(media.Height)},
|
||||||
|
templates.OpenGraphItem{Property: "og:video:type", Value: media.MimeType},
|
||||||
|
templates.OpenGraphItem{Name: "twitter:card", Value: "player"},
|
||||||
|
)
|
||||||
|
case templates.TimelineItemMediaTypeAudio:
|
||||||
|
opengraph = append(opengraph,
|
||||||
|
templates.OpenGraphItem{Property: "og:audio", Value: media.AssetUrl},
|
||||||
|
templates.OpenGraphItem{Property: "og:audio:type", Value: media.MimeType},
|
||||||
|
templates.OpenGraphItem{Name: "twitter:card", Value: "player"},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
opengraph = append(opengraph, opengraphImage...)
|
opengraph = append(opengraph, media.ExtraOpenGraphItems...)
|
||||||
} else if snippet.Type == templates.TimelineTypeSnippetVideo {
|
|
||||||
opengraphVideo := []templates.OpenGraphItem{
|
|
||||||
{Property: "og:video", Value: snippet.AssetUrl},
|
|
||||||
{Property: "og:video:width", Value: strconv.Itoa(snippet.Width)},
|
|
||||||
{Property: "og:video:height", Value: strconv.Itoa(snippet.Height)},
|
|
||||||
{Property: "og:video:type", Value: snippet.MimeType},
|
|
||||||
{Name: "twitter:card", Value: "player"},
|
|
||||||
}
|
|
||||||
opengraph = append(opengraph, opengraphVideo...)
|
|
||||||
} else if snippet.Type == templates.TimelineTypeSnippetAudio {
|
|
||||||
opengraphAudio := []templates.OpenGraphItem{
|
|
||||||
{Property: "og:audio", Value: snippet.AssetUrl},
|
|
||||||
{Property: "og:audio:type", Value: snippet.MimeType},
|
|
||||||
{Name: "twitter:card", Value: "player"},
|
|
||||||
}
|
|
||||||
opengraph = append(opengraph, opengraphAudio...)
|
|
||||||
} else if snippet.Type == templates.TimelineTypeSnippetYoutube {
|
|
||||||
opengraphYoutube := []templates.OpenGraphItem{
|
|
||||||
{Property: "og:video", Value: fmt.Sprintf("https://youtube.com/watch?v=%s", snippet.YoutubeID)},
|
|
||||||
{Name: "twitter:card", Value: "player"},
|
|
||||||
}
|
|
||||||
opengraph = append(opengraph, opengraphYoutube...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
baseData := getBaseData(
|
baseData := getBaseData(
|
||||||
|
|
|
@ -1,136 +1,174 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
|
"git.handmade.network/hmn/hmn/src/logging"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
var TimelineTypeMap = map[models.ThreadType][]templates.TimelineType{
|
type TimelineTypeTitles struct {
|
||||||
// { First post , Subsequent post }
|
TypeTitleFirst string
|
||||||
models.ThreadTypeProjectBlogPost: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
TypeTitleNotFirst string
|
||||||
models.ThreadTypeForumPost: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
|
FilterTitle string
|
||||||
}
|
}
|
||||||
|
|
||||||
var TimelineItemClassMap = map[templates.TimelineType]string{
|
var TimelineTypeTitleMap = map[models.ThreadType]TimelineTypeTitles{
|
||||||
templates.TimelineTypeUnknown: "",
|
models.ThreadTypeProjectBlogPost: {"New blog post", "Blog comment", "Blogs"},
|
||||||
|
models.ThreadTypeForumPost: {"New forum thread", "Forum reply", "Forums"},
|
||||||
templates.TimelineTypeForumThread: "forums",
|
|
||||||
templates.TimelineTypeForumReply: "forums",
|
|
||||||
|
|
||||||
templates.TimelineTypeBlogPost: "blogs",
|
|
||||||
templates.TimelineTypeBlogComment: "blogs",
|
|
||||||
|
|
||||||
templates.TimelineTypeSnippetImage: "snippets",
|
|
||||||
templates.TimelineTypeSnippetVideo: "snippets",
|
|
||||||
templates.TimelineTypeSnippetAudio: "snippets",
|
|
||||||
templates.TimelineTypeSnippetYoutube: "snippets",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var TimelineTypeTitleMap = map[templates.TimelineType]string{
|
func PostToTimelineItem(
|
||||||
templates.TimelineTypeUnknown: "",
|
lineageBuilder *models.SubforumLineageBuilder,
|
||||||
|
post *models.Post,
|
||||||
templates.TimelineTypeForumThread: "New forum thread",
|
thread *models.Thread,
|
||||||
templates.TimelineTypeForumReply: "Forum reply",
|
project *models.Project,
|
||||||
|
owner *models.User,
|
||||||
templates.TimelineTypeBlogPost: "New blog post",
|
currentTheme string,
|
||||||
templates.TimelineTypeBlogComment: "Blog comment",
|
) templates.TimelineItem {
|
||||||
|
item := templates.TimelineItem{
|
||||||
templates.TimelineTypeSnippetImage: "Snippet",
|
Date: post.PostDate,
|
||||||
templates.TimelineTypeSnippetVideo: "Snippet",
|
Title: thread.Title,
|
||||||
templates.TimelineTypeSnippetAudio: "Snippet",
|
Breadcrumbs: GenericThreadBreadcrumbs(lineageBuilder, project, thread),
|
||||||
templates.TimelineTypeSnippetYoutube: "Snippet",
|
Url: UrlForGenericPost(thread, post, lineageBuilder, project.Slug),
|
||||||
}
|
|
||||||
|
|
||||||
func PostToTimelineItem(lineageBuilder *models.SubforumLineageBuilder, post *models.Post, thread *models.Thread, project *models.Project, owner *models.User, currentTheme string) templates.TimelineItem {
|
|
||||||
itemType := templates.TimelineTypeUnknown
|
|
||||||
typeByCatKind, found := TimelineTypeMap[post.ThreadType]
|
|
||||||
if found {
|
|
||||||
isNotFirst := 0
|
|
||||||
if thread.FirstID != post.ID {
|
|
||||||
isNotFirst = 1
|
|
||||||
}
|
|
||||||
itemType = typeByCatKind[isNotFirst]
|
|
||||||
}
|
|
||||||
|
|
||||||
return templates.TimelineItem{
|
|
||||||
Type: itemType,
|
|
||||||
TypeTitle: TimelineTypeTitleMap[itemType],
|
|
||||||
Class: TimelineItemClassMap[itemType],
|
|
||||||
Date: post.PostDate,
|
|
||||||
Url: UrlForGenericPost(thread, post, lineageBuilder, project.Slug),
|
|
||||||
|
|
||||||
OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme),
|
OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme),
|
||||||
OwnerName: owner.BestName(),
|
OwnerName: owner.BestName(),
|
||||||
OwnerUrl: hmnurl.BuildUserProfile(owner.Username),
|
OwnerUrl: hmnurl.BuildUserProfile(owner.Username),
|
||||||
Description: "", // NOTE(asaf): No description for posts
|
|
||||||
|
|
||||||
Title: thread.Title,
|
|
||||||
Breadcrumbs: GenericThreadBreadcrumbs(lineageBuilder, project, thread),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if typeTitles, ok := TimelineTypeTitleMap[post.ThreadType]; ok {
|
||||||
|
if thread.FirstID == post.ID {
|
||||||
|
item.TypeTitle = typeTitles.TypeTitleFirst
|
||||||
|
} else {
|
||||||
|
item.TypeTitle = typeTitles.TypeTitleNotFirst
|
||||||
|
}
|
||||||
|
item.FilterTitle = typeTitles.FilterTitle
|
||||||
|
} else {
|
||||||
|
logging.Warn().
|
||||||
|
Int("postID", post.ID).
|
||||||
|
Int("threadType", int(post.ThreadType)).
|
||||||
|
Msg("unknown thread type for post")
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
var YoutubeRegex = regexp.MustCompile(`(?i)youtube\.com/watch\?.*v=(?P<videoid>[^/&]+)`)
|
func SnippetToTimelineItem(
|
||||||
var YoutubeShortRegex = regexp.MustCompile(`(?i)youtu\.be/(?P<videoid>[^/]+)`)
|
snippet *models.Snippet,
|
||||||
|
asset *models.Asset,
|
||||||
|
discordMessage *models.DiscordMessage,
|
||||||
|
owner *models.User,
|
||||||
|
currentTheme string,
|
||||||
|
) templates.TimelineItem {
|
||||||
|
item := templates.TimelineItem{
|
||||||
|
Date: snippet.When,
|
||||||
|
FilterTitle: "Snippets",
|
||||||
|
Url: hmnurl.BuildSnippet(snippet.ID),
|
||||||
|
|
||||||
func SnippetToTimelineItem(snippet *models.Snippet, asset *models.Asset, discordMessage *models.DiscordMessage, owner *models.User, currentTheme string) templates.TimelineItem {
|
OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme),
|
||||||
itemType := templates.TimelineTypeUnknown
|
OwnerName: owner.BestName(),
|
||||||
youtubeId := ""
|
OwnerUrl: hmnurl.BuildUserProfile(owner.Username),
|
||||||
assetUrl := ""
|
|
||||||
mimeType := ""
|
|
||||||
width := 0
|
|
||||||
height := 0
|
|
||||||
discordMessageUrl := ""
|
|
||||||
|
|
||||||
if asset == nil {
|
Description: template.HTML(snippet.DescriptionHtml),
|
||||||
match := YoutubeRegex.FindStringSubmatch(*snippet.Url)
|
|
||||||
index := YoutubeRegex.SubexpIndex("videoid")
|
CanShowcase: true,
|
||||||
if match == nil {
|
}
|
||||||
match = YoutubeShortRegex.FindStringSubmatch(*snippet.Url)
|
|
||||||
index = YoutubeShortRegex.SubexpIndex("videoid")
|
if asset != nil {
|
||||||
}
|
|
||||||
if match != nil {
|
|
||||||
youtubeId = match[index]
|
|
||||||
itemType = templates.TimelineTypeSnippetYoutube
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if strings.HasPrefix(asset.MimeType, "image/") {
|
if strings.HasPrefix(asset.MimeType, "image/") {
|
||||||
itemType = templates.TimelineTypeSnippetImage
|
item.EmbedMedia = append(item.EmbedMedia, imageMediaItem(asset))
|
||||||
} else if strings.HasPrefix(asset.MimeType, "video/") {
|
} else if strings.HasPrefix(asset.MimeType, "video/") {
|
||||||
itemType = templates.TimelineTypeSnippetVideo
|
item.EmbedMedia = append(item.EmbedMedia, videoMediaItem(asset))
|
||||||
} else if strings.HasPrefix(asset.MimeType, "audio/") {
|
} else if strings.HasPrefix(asset.MimeType, "audio/") {
|
||||||
itemType = templates.TimelineTypeSnippetAudio
|
item.EmbedMedia = append(item.EmbedMedia, audioMediaItem(asset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if snippet.Url != nil {
|
||||||
|
url := *snippet.Url
|
||||||
|
if videoId := getYoutubeVideoID(url); videoId != "" {
|
||||||
|
item.EmbedMedia = append(item.EmbedMedia, youtubeMediaItem(videoId))
|
||||||
|
item.CanShowcase = false
|
||||||
}
|
}
|
||||||
assetUrl = hmnurl.BuildS3Asset(asset.S3Key)
|
|
||||||
mimeType = asset.MimeType
|
|
||||||
width = asset.Width
|
|
||||||
height = asset.Height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if discordMessage != nil {
|
if discordMessage != nil {
|
||||||
discordMessageUrl = discordMessage.Url
|
item.DiscordMessageUrl = discordMessage.Url
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates.TimelineItem{
|
return item
|
||||||
Type: itemType,
|
}
|
||||||
Class: TimelineItemClassMap[itemType],
|
|
||||||
Date: snippet.When,
|
|
||||||
Url: hmnurl.BuildSnippet(snippet.ID),
|
|
||||||
|
|
||||||
OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme),
|
var youtubeRegexes = [...]*regexp.Regexp{
|
||||||
OwnerName: owner.BestName(),
|
regexp.MustCompile(`(?i)youtube\.com/watch\?.*v=(?P<videoid>[^/&]+)`),
|
||||||
OwnerUrl: hmnurl.BuildUserProfile(owner.Username),
|
regexp.MustCompile(`(?i)youtu\.be/(?P<videoid>[^/]+)`),
|
||||||
Description: template.HTML(snippet.DescriptionHtml),
|
}
|
||||||
|
|
||||||
DiscordMessageUrl: discordMessageUrl,
|
func getYoutubeVideoID(url string) string {
|
||||||
Width: width,
|
for _, regex := range youtubeRegexes {
|
||||||
Height: height,
|
match := regex.FindStringSubmatch(url)
|
||||||
AssetUrl: assetUrl,
|
if match != nil {
|
||||||
MimeType: mimeType,
|
return match[regex.SubexpIndex("videoid")]
|
||||||
YoutubeID: youtubeId,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageMediaItem(asset *models.Asset) templates.TimelineItemMedia {
|
||||||
|
assetUrl := hmnurl.BuildS3Asset(asset.S3Key)
|
||||||
|
|
||||||
|
return templates.TimelineItemMedia{
|
||||||
|
Type: templates.TimelineItemMediaTypeImage,
|
||||||
|
AssetUrl: assetUrl,
|
||||||
|
ThumbnailUrl: assetUrl, // TODO: Use smaller thumbnails?
|
||||||
|
MimeType: asset.MimeType,
|
||||||
|
Width: asset.Width,
|
||||||
|
Height: asset.Height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func videoMediaItem(asset *models.Asset) templates.TimelineItemMedia {
|
||||||
|
assetUrl := hmnurl.BuildS3Asset(asset.S3Key)
|
||||||
|
|
||||||
|
return templates.TimelineItemMedia{
|
||||||
|
Type: templates.TimelineItemMediaTypeVideo,
|
||||||
|
AssetUrl: assetUrl,
|
||||||
|
// TODO: Use image thumbnails
|
||||||
|
MimeType: asset.MimeType,
|
||||||
|
Width: asset.Width,
|
||||||
|
Height: asset.Height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func audioMediaItem(asset *models.Asset) templates.TimelineItemMedia {
|
||||||
|
assetUrl := hmnurl.BuildS3Asset(asset.S3Key)
|
||||||
|
|
||||||
|
return templates.TimelineItemMedia{
|
||||||
|
Type: templates.TimelineItemMediaTypeAudio,
|
||||||
|
AssetUrl: assetUrl,
|
||||||
|
MimeType: asset.MimeType,
|
||||||
|
Width: asset.Width,
|
||||||
|
Height: asset.Height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func youtubeMediaItem(videoId string) templates.TimelineItemMedia {
|
||||||
|
return templates.TimelineItemMedia{
|
||||||
|
Type: templates.TimelineItemMediaTypeEmbed,
|
||||||
|
EmbedHTML: template.HTML(fmt.Sprintf(
|
||||||
|
`<iframe src="https://www.youtube-nocookie.com/embed/%s" allow="accelerometer; encrypted-media; gyroscope;" allowfullscreen frameborder="0"></iframe>`,
|
||||||
|
template.HTMLEscapeString(videoId),
|
||||||
|
)),
|
||||||
|
ExtraOpenGraphItems: []templates.OpenGraphItem{
|
||||||
|
{Property: "og:video", Value: fmt.Sprintf("https://youtube.com/watch?v=%s", videoId)},
|
||||||
|
{Name: "twitter:card", Value: "player"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,6 @@ type UserProfileTemplateData struct {
|
||||||
ProfileUserLinks []templates.Link
|
ProfileUserLinks []templates.Link
|
||||||
ProfileUserProjects []templates.Project
|
ProfileUserProjects []templates.Project
|
||||||
TimelineItems []templates.TimelineItem
|
TimelineItems []templates.TimelineItem
|
||||||
NumForums int
|
|
||||||
NumBlogs int
|
|
||||||
NumSnippets int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserProfile(c *RequestContext) ResponseData {
|
func UserProfile(c *RequestContext) ResponseData {
|
||||||
|
@ -166,42 +163,27 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
c.Perf.StartBlock("PROFILE", "Construct timeline items")
|
c.Perf.StartBlock("PROFILE", "Construct timeline items")
|
||||||
timelineItems := make([]templates.TimelineItem, 0, len(posts)+len(snippetQuerySlice))
|
timelineItems := make([]templates.TimelineItem, 0, len(posts)+len(snippetQuerySlice))
|
||||||
numForums := 0
|
|
||||||
numBlogs := 0
|
|
||||||
numSnippets := len(snippetQuerySlice)
|
|
||||||
|
|
||||||
for _, post := range posts {
|
for _, post := range posts {
|
||||||
timelineItem := PostToTimelineItem(
|
timelineItems = append(timelineItems, PostToTimelineItem(
|
||||||
lineageBuilder,
|
lineageBuilder,
|
||||||
&post.Post,
|
&post.Post,
|
||||||
&post.Thread,
|
&post.Thread,
|
||||||
&post.Project,
|
&post.Project,
|
||||||
profileUser,
|
profileUser,
|
||||||
c.Theme,
|
c.Theme,
|
||||||
)
|
))
|
||||||
switch timelineItem.Type {
|
|
||||||
case templates.TimelineTypeForumThread, templates.TimelineTypeForumReply:
|
|
||||||
numForums += 1
|
|
||||||
case templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment:
|
|
||||||
numBlogs += 1
|
|
||||||
}
|
|
||||||
if timelineItem.Type != templates.TimelineTypeUnknown {
|
|
||||||
timelineItems = append(timelineItems, timelineItem)
|
|
||||||
} else {
|
|
||||||
c.Logger.Warn().Int("post ID", post.Post.ID).Msg("Unknown timeline item type for post")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, snippetRow := range snippetQuerySlice {
|
for _, snippetRow := range snippetQuerySlice {
|
||||||
snippetData := snippetRow.(*snippetQuery)
|
snippetData := snippetRow.(*snippetQuery)
|
||||||
timelineItem := SnippetToTimelineItem(
|
timelineItems = append(timelineItems, SnippetToTimelineItem(
|
||||||
&snippetData.Snippet,
|
&snippetData.Snippet,
|
||||||
snippetData.Asset,
|
snippetData.Asset,
|
||||||
snippetData.DiscordMessage,
|
snippetData.DiscordMessage,
|
||||||
profileUser,
|
profileUser,
|
||||||
c.Theme,
|
c.Theme,
|
||||||
)
|
))
|
||||||
timelineItems = append(timelineItems, timelineItem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Perf.StartBlock("PROFILE", "Sort timeline")
|
c.Perf.StartBlock("PROFILE", "Sort timeline")
|
||||||
|
@ -223,9 +205,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
ProfileUserLinks: profileUserLinks,
|
ProfileUserLinks: profileUserLinks,
|
||||||
ProfileUserProjects: templateProjects,
|
ProfileUserProjects: templateProjects,
|
||||||
TimelineItems: timelineItems,
|
TimelineItems: timelineItems,
|
||||||
NumForums: numForums,
|
|
||||||
NumBlogs: numBlogs,
|
|
||||||
NumSnippets: numSnippets,
|
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue