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,15 +54,21 @@
</author>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<div>
{{ .Description }}
{{ if snippetimage . }}
</div>
{{ range .EmbedMedia }}
<div>
{{ if eq .Type mediaimage }}
<img src="{{ .AssetUrl }}"/>
{{ else if snippetvideo . }}
{{ else if eq .Type mediavideo }}
<video src="{{ .AssetUrl }}" controls="true"/>
{{ else if snippetaudio . }}
{{ else if eq .Type mediaaudio }}
<audio src="{{ .AssetUrl }}" controls="true"/>
{{ end }}
</div>
{{ end }}
</div>
</content>
</entry>
{{ end }}

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 }}
{{ 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">
<iframe class="aspect-ratio--object" src="https://www.youtube-nocookie.com/embed/{{ .Snippet.YoutubeID }}" allow="accelerometer; encrypted-media; gyroscope;" allowfullscreen frameborder="0" />
{{ .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>
</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
Title string
TypeTitle string
FilterTitle string
Breadcrumbs []Breadcrumb
Url string
UUID string
DiscordMessageUrl string
OwnerAvatarUrl string
OwnerName string
OwnerUrl string
Description template.HTML
TruncateDescription bool
DiscordMessageUrl string
Width int
Height int
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
YoutubeID string
Title string
Breadcrumbs []Breadcrumb
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],
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),
}
}
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 {
itemType := templates.TimelineTypeUnknown
youtubeId := ""
assetUrl := ""
mimeType := ""
width := 0
height := 0
discordMessageUrl := ""
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
}
if typeTitles, ok := TimelineTypeTitleMap[post.ThreadType]; ok {
if thread.FirstID == post.ID {
item.TypeTitle = typeTitles.TypeTitleFirst
} else {
if strings.HasPrefix(asset.MimeType, "image/") {
itemType = templates.TimelineTypeSnippetImage
} else if strings.HasPrefix(asset.MimeType, "video/") {
itemType = templates.TimelineTypeSnippetVideo
} else if strings.HasPrefix(asset.MimeType, "audio/") {
itemType = templates.TimelineTypeSnippetAudio
item.TypeTitle = typeTitles.TypeTitleNotFirst
}
assetUrl = hmnurl.BuildS3Asset(asset.S3Key)
mimeType = asset.MimeType
width = asset.Width
height = asset.Height
item.FilterTitle = typeTitles.FilterTitle
} else {
logging.Warn().
Int("postID", post.ID).
Int("threadType", int(post.ThreadType)).
Msg("unknown thread type for post")
}
if discordMessage != nil {
discordMessageUrl = discordMessage.Url
return item
}
return templates.TimelineItem{
Type: itemType,
Class: TimelineItemClassMap[itemType],
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),
OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme),
OwnerName: owner.BestName(),
OwnerUrl: hmnurl.BuildUserProfile(owner.Username),
Description: template.HTML(snippet.DescriptionHtml),
DiscordMessageUrl: discordMessageUrl,
Width: width,
Height: height,
CanShowcase: true,
}
if asset != nil {
if strings.HasPrefix(asset.MimeType, "image/") {
item.EmbedMedia = append(item.EmbedMedia, imageMediaItem(asset))
} else if strings.HasPrefix(asset.MimeType, "video/") {
item.EmbedMedia = append(item.EmbedMedia, videoMediaItem(asset))
} else if strings.HasPrefix(asset.MimeType, "audio/") {
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
}
}
if discordMessage != nil {
item.DiscordMessageUrl = discordMessage.Url
}
return item
}
var youtubeRegexes = [...]*regexp.Regexp{
regexp.MustCompile(`(?i)youtube\.com/watch\?.*v=(?P<videoid>[^/&]+)`),
regexp.MustCompile(`(?i)youtu\.be/(?P<videoid>[^/]+)`),
}
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,
MimeType: mimeType,
YoutubeID: youtubeId,
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
}