Rejigger timelines to avoid explicit types

This commit is contained in:
Ben Visness 2021-10-23 17:28:06 -05:00
parent 6176744462
commit 09e6a15085
18 changed files with 357 additions and 326 deletions

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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%;

View File

@ -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":"`)

View File

@ -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>

View File

@ -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 }}

View File

@ -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>

View File

@ -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 }}

View File

@ -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 },
}

View File

@ -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 {

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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"},
},
}
}

View File

@ -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
}