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