Follow infrastructure
This commit is contained in:
parent
3ff6ba6563
commit
6a28660407
|
@ -62,8 +62,8 @@ func NewConnWithConfig(cfg config.PostgresConfig) *pgx.Conn {
|
||||||
pgcfg, err := pgx.ParseConfig(cfg.DSN())
|
pgcfg, err := pgx.ParseConfig(cfg.DSN())
|
||||||
|
|
||||||
pgcfg.Tracer = &tracelog.TraceLog{
|
pgcfg.Tracer = &tracelog.TraceLog{
|
||||||
zerologadapter.NewLogger(log.Logger),
|
Logger: zerologadapter.NewLogger(log.Logger),
|
||||||
cfg.LogLevel,
|
LogLevel: cfg.LogLevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := pgx.ConnectConfig(context.Background(), pgcfg)
|
conn, err := pgx.ConnectConfig(context.Background(), pgcfg)
|
||||||
|
@ -88,8 +88,8 @@ func NewConnPoolWithConfig(cfg config.PostgresConfig) *pgxpool.Pool {
|
||||||
pgcfg.MinConns = cfg.MinConn
|
pgcfg.MinConns = cfg.MinConn
|
||||||
pgcfg.MaxConns = cfg.MaxConn
|
pgcfg.MaxConns = cfg.MaxConn
|
||||||
pgcfg.ConnConfig.Tracer = &tracelog.TraceLog{
|
pgcfg.ConnConfig.Tracer = &tracelog.TraceLog{
|
||||||
zerologadapter.NewLogger(log.Logger),
|
Logger: zerologadapter.NewLogger(log.Logger),
|
||||||
cfg.LogLevel,
|
LogLevel: cfg.LogLevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := pgxpool.NewWithConfig(context.Background(), pgcfg)
|
conn, err := pgxpool.NewWithConfig(context.Background(), pgcfg)
|
||||||
|
|
|
@ -937,6 +937,28 @@ func BuildTwitchEventSubCallback() string {
|
||||||
|
|
||||||
var RegexTwitchDebugPage = regexp.MustCompile("^/twitch_debug$")
|
var RegexTwitchDebugPage = regexp.MustCompile("^/twitch_debug$")
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Following
|
||||||
|
*/
|
||||||
|
|
||||||
|
var RegexFollowingTest = regexp.MustCompile("^/following$")
|
||||||
|
|
||||||
|
func BuildFollowingTest() string {
|
||||||
|
return Url("/following", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexFollowUser = regexp.MustCompile("^/follow/user$")
|
||||||
|
|
||||||
|
func BuildFollowUser() string {
|
||||||
|
return Url("/follow/user", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexFollowProject = regexp.MustCompile("^/follow/project$")
|
||||||
|
|
||||||
|
func BuildFollowProject() string {
|
||||||
|
return Url("/follow/project", nil)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* User assets
|
* User assets
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerMigration(AddFollower{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddFollower struct{}
|
||||||
|
|
||||||
|
func (m AddFollower) Version() types.MigrationVersion {
|
||||||
|
return types.MigrationVersion(time.Date(2024, 5, 16, 19, 41, 34, 0, time.UTC))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m AddFollower) Name() string {
|
||||||
|
return "AddFollower"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m AddFollower) Description() string {
|
||||||
|
return "Add follower table"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m AddFollower) Up(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
_, err := tx.Exec(ctx,
|
||||||
|
`
|
||||||
|
CREATE TABLE follower (
|
||||||
|
user_id int NOT NULL,
|
||||||
|
following_user_id int REFERENCES hmn_user (id) ON DELETE CASCADE,
|
||||||
|
following_project_id int REFERENCES project (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX follower_user_id ON follower(user_id);
|
||||||
|
CREATE UNIQUE INDEX follower_following_user ON follower (user_id, following_user_id);
|
||||||
|
CREATE UNIQUE INDEX follower_following_project ON follower (user_id, following_project_id);
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m AddFollower) Down(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
_, err := tx.Exec(ctx,
|
||||||
|
`
|
||||||
|
DROP INDEX follower_following_user;
|
||||||
|
DROP INDEX follower_following_project;
|
||||||
|
DROP INDEX follower_user_id;
|
||||||
|
DROP TABLE follower;
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="flex flex-column">
|
||||||
|
{{ range .TimelineItems }}
|
||||||
|
{{ template "timeline_item.html" . }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
|
@ -43,6 +43,34 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .FollowUrl }}
|
||||||
|
<a id="follow_link" class="db" href="javascript:;">{{ if .Following }}Unfollow{{ else }}Follow{{ end }}</a>
|
||||||
|
<script>
|
||||||
|
const followLink = document.getElementById("follow_link");
|
||||||
|
let following = {{ .Following }};
|
||||||
|
followLink.addEventListener("click", async function() {
|
||||||
|
followLink.disabled = true;
|
||||||
|
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.set("csrf_token", "{{ .Session.CSRFToken }}");
|
||||||
|
formData.set("project_id", "{{ .Project.ID }}");
|
||||||
|
if (following) {
|
||||||
|
formData.set("unfollow", "true");
|
||||||
|
}
|
||||||
|
let result = await fetch("{{ .FollowUrl }}", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
redirect: "error",
|
||||||
|
});
|
||||||
|
if (result.ok) {
|
||||||
|
following = !following;
|
||||||
|
followLink.textContent = (following ? "Unfollow" : "Follow");
|
||||||
|
}
|
||||||
|
|
||||||
|
followLink.disabled = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
||||||
{{ if or .Header.Project.CanEdit (gt (len .RecentActivity) 0) }}
|
{{ if or .Header.Project.CanEdit (gt (len .RecentActivity) 0) }}
|
||||||
<hr class="w-100 mv4">
|
<hr class="w-100 mv4">
|
||||||
<div class="w-100 flex g3">
|
<div class="w-100 flex g3">
|
||||||
|
|
|
@ -43,6 +43,34 @@
|
||||||
{{ with or .ProfileUser.Bio .ProfileUser.Blurb }}
|
{{ with or .ProfileUser.Bio .ProfileUser.Blurb }}
|
||||||
<div class="mb3">{{ . }}</div>
|
<div class="mb3">{{ . }}</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .FollowUrl }}
|
||||||
|
<a id="follow_link" class="db" href="javascript:;">{{ if .Following }}Unfollow{{ else }}Follow{{ end }}</a>
|
||||||
|
<script>
|
||||||
|
const followLink = document.getElementById("follow_link");
|
||||||
|
let following = {{ .Following }};
|
||||||
|
followLink.addEventListener("click", async function() {
|
||||||
|
followLink.disabled = true;
|
||||||
|
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.set("csrf_token", "{{ .Session.CSRFToken }}");
|
||||||
|
formData.set("user_id", "{{ .ProfileUser.ID }}");
|
||||||
|
if (following) {
|
||||||
|
formData.set("unfollow", "true");
|
||||||
|
}
|
||||||
|
let result = await fetch("{{ .FollowUrl }}", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
redirect: "error",
|
||||||
|
});
|
||||||
|
if (result.ok) {
|
||||||
|
following = !following;
|
||||||
|
followLink.textContent = (following ? "Unfollow" : "Follow");
|
||||||
|
}
|
||||||
|
|
||||||
|
followLink.disabled = false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
||||||
<div class="w-100 w-auto-ns w-100-l">
|
<div class="w-100 w-auto-ns w-100-l">
|
||||||
{{ if .ProfileUser.Email }}
|
{{ if .ProfileUser.Email }}
|
||||||
<div class="pair flex">
|
<div class="pair flex">
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
package website
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||||
|
"git.handmade.network/hmn/hmn/src/logging"
|
||||||
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FollowingTest(c *RequestContext) ResponseData {
|
||||||
|
type Follower struct {
|
||||||
|
UserID int `db:"user_id"`
|
||||||
|
FollowingUserID *int `db:"following_user_id"`
|
||||||
|
FollowingProjectID *int `db:"following_project_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
following, err := db.Query[Follower](c, c.Conn, `
|
||||||
|
SELECT $columns
|
||||||
|
FROM follower
|
||||||
|
WHERE user_id = $1
|
||||||
|
`, c.CurrentUser.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch follow data"))
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDs := make([]int, 0, len(following))
|
||||||
|
userIDs := make([]int, 0, len(following))
|
||||||
|
for _, f := range following {
|
||||||
|
if f.FollowingProjectID != nil {
|
||||||
|
projectIDs = append(projectIDs, *f.FollowingProjectID)
|
||||||
|
}
|
||||||
|
if f.FollowingUserID != nil {
|
||||||
|
userIDs = append(userIDs, *f.FollowingUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userSnippets []hmndata.SnippetAndStuff
|
||||||
|
var projectSnippets []hmndata.SnippetAndStuff
|
||||||
|
var userPosts []hmndata.PostAndStuff
|
||||||
|
var projectPosts []hmndata.PostAndStuff
|
||||||
|
|
||||||
|
if len(following) > 0 {
|
||||||
|
projects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
|
||||||
|
ProjectIDs: projectIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch projects"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(asaf): The original projectIDs might container hidden/abandoned projects,
|
||||||
|
// so we recreate it after the projects get filtered by FetchProjects.
|
||||||
|
projectIDs = projectIDs[0:0]
|
||||||
|
for _, p := range projects {
|
||||||
|
projectIDs = append(projectIDs, p.Project.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
userSnippets, err = hmndata.FetchSnippets(c, c.Conn, c.CurrentUser, hmndata.SnippetQuery{
|
||||||
|
OwnerIDs: userIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user snippets"))
|
||||||
|
}
|
||||||
|
projectSnippets, err = hmndata.FetchSnippets(c, c.Conn, c.CurrentUser, hmndata.SnippetQuery{
|
||||||
|
ProjectIDs: projectIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project snippets"))
|
||||||
|
}
|
||||||
|
|
||||||
|
userPosts, err = hmndata.FetchPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
|
||||||
|
UserIDs: userIDs,
|
||||||
|
SortDescending: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user posts"))
|
||||||
|
}
|
||||||
|
projectPosts, err = hmndata.FetchPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
|
||||||
|
ProjectIDs: projectIDs,
|
||||||
|
SortDescending: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project posts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Perf.StartBlock("FOLLOWING", "Construct timeline items")
|
||||||
|
timelineItems := make([]templates.TimelineItem, 0, len(userSnippets)+len(projectSnippets)+len(userPosts)+len(projectPosts))
|
||||||
|
|
||||||
|
if len(userPosts) > 0 || len(projectPosts) > 0 {
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
|
subforumTree := models.GetFullSubforumTree(c, c.Conn)
|
||||||
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
for _, post := range userPosts {
|
||||||
|
timelineItems = append(timelineItems, PostToTimelineItem(
|
||||||
|
hmndata.UrlContextForProject(&post.Project),
|
||||||
|
lineageBuilder,
|
||||||
|
&post.Post,
|
||||||
|
&post.Thread,
|
||||||
|
post.Author,
|
||||||
|
c.Theme,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
for _, post := range projectPosts {
|
||||||
|
timelineItems = append(timelineItems, PostToTimelineItem(
|
||||||
|
hmndata.UrlContextForProject(&post.Project),
|
||||||
|
lineageBuilder,
|
||||||
|
&post.Post,
|
||||||
|
&post.Thread,
|
||||||
|
post.Author,
|
||||||
|
c.Theme,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range userSnippets {
|
||||||
|
item := SnippetToTimelineItem(
|
||||||
|
&s.Snippet,
|
||||||
|
s.Asset,
|
||||||
|
s.DiscordMessage,
|
||||||
|
s.Projects,
|
||||||
|
s.Owner,
|
||||||
|
c.Theme,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
item.SmallInfo = true
|
||||||
|
timelineItems = append(timelineItems, item)
|
||||||
|
}
|
||||||
|
for _, s := range projectSnippets {
|
||||||
|
item := SnippetToTimelineItem(
|
||||||
|
&s.Snippet,
|
||||||
|
s.Asset,
|
||||||
|
s.DiscordMessage,
|
||||||
|
s.Projects,
|
||||||
|
s.Owner,
|
||||||
|
c.Theme,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
item.SmallInfo = true
|
||||||
|
timelineItems = append(timelineItems, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(asaf): Show when they're live on twitch
|
||||||
|
|
||||||
|
c.Perf.StartBlock("FOLLOWING", "Sort timeline")
|
||||||
|
sort.Slice(timelineItems, func(i, j int) bool {
|
||||||
|
return timelineItems[j].Date.Before(timelineItems[i].Date)
|
||||||
|
})
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
type FollowingTestData struct {
|
||||||
|
templates.BaseData
|
||||||
|
TimelineItems []templates.TimelineItem
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
res.MustWriteTemplate("following_test.html", FollowingTestData{
|
||||||
|
BaseData: getBaseDataAutocrumb(c, "Following test"),
|
||||||
|
TimelineItems: timelineItems,
|
||||||
|
}, c.Perf)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func FollowUser(c *RequestContext) ResponseData {
|
||||||
|
err := c.Req.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse form data"))
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDStr := c.Req.Form.Get("user_id")
|
||||||
|
unfollowStr := c.Req.Form.Get("unfollow")
|
||||||
|
|
||||||
|
userID, err := strconv.Atoi(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to parse user_id field"))
|
||||||
|
}
|
||||||
|
unfollow := unfollowStr != ""
|
||||||
|
|
||||||
|
if unfollow {
|
||||||
|
_, err = c.Conn.Exec(c, `
|
||||||
|
DELETE FROM follower
|
||||||
|
WHERE user_id = $1 AND following_user_id = $2
|
||||||
|
`, c.CurrentUser.ID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to unfollow user"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = c.Conn.Exec(c, `
|
||||||
|
INSERT INTO follower (user_id, following_user_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`, c.CurrentUser.ID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to follow user"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
addCORSHeaders(c, &res)
|
||||||
|
res.WriteHeader(http.StatusNoContent)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func FollowProject(c *RequestContext) ResponseData {
|
||||||
|
err := c.Req.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse form data"))
|
||||||
|
}
|
||||||
|
|
||||||
|
projectIDStr := c.Req.Form.Get("project_id")
|
||||||
|
unfollowStr := c.Req.Form.Get("unfollow")
|
||||||
|
|
||||||
|
projectID, err := strconv.Atoi(projectIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to parse project_id field"))
|
||||||
|
}
|
||||||
|
unfollow := unfollowStr != ""
|
||||||
|
|
||||||
|
if unfollow {
|
||||||
|
_, err = c.Conn.Exec(c, `
|
||||||
|
DELETE FROM follower
|
||||||
|
WHERE user_id = $1 AND following_project_id = $2
|
||||||
|
`, c.CurrentUser.ID, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to unfollow project"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logging.Debug().Int("userID", c.CurrentUser.ID).Int("projectID", projectID).Msg("thing")
|
||||||
|
_, err = c.Conn.Exec(c, `
|
||||||
|
INSERT INTO follower (user_id, following_project_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`, c.CurrentUser.ID, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to follow project"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
addCORSHeaders(c, &res)
|
||||||
|
res.WriteHeader(http.StatusNoContent)
|
||||||
|
return res
|
||||||
|
}
|
|
@ -319,6 +319,20 @@ func jamLink(jamSlug string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectHomepageData struct {
|
||||||
|
templates.BaseData
|
||||||
|
Project templates.Project
|
||||||
|
Owners []templates.User
|
||||||
|
Screenshots []string
|
||||||
|
ProjectLinks []templates.Link
|
||||||
|
Licenses []templates.Link
|
||||||
|
RecentActivity []templates.TimelineItem
|
||||||
|
SnippetEdit templates.SnippetEdit
|
||||||
|
|
||||||
|
FollowUrl string
|
||||||
|
Following bool
|
||||||
|
}
|
||||||
|
|
||||||
func ProjectHomepage(c *RequestContext) ResponseData {
|
func ProjectHomepage(c *RequestContext) ResponseData {
|
||||||
maxRecentActivity := 15
|
maxRecentActivity := 15
|
||||||
|
|
||||||
|
@ -389,6 +403,9 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
||||||
NamedLinks, UnnamedLinks []templates.Link
|
NamedLinks, UnnamedLinks []templates.Link
|
||||||
RecentActivity []templates.TimelineItem
|
RecentActivity []templates.TimelineItem
|
||||||
SnippetEdit templates.SnippetEdit
|
SnippetEdit templates.SnippetEdit
|
||||||
|
|
||||||
|
FollowUrl string
|
||||||
|
Following bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var templateData ProjectHomepageData
|
var templateData ProjectHomepageData
|
||||||
|
@ -500,6 +517,8 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
||||||
})
|
})
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
followUrl := ""
|
||||||
|
following := false
|
||||||
if c.CurrentUser != nil {
|
if c.CurrentUser != nil {
|
||||||
userProjects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
|
userProjects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
|
||||||
OwnerIDs: []int{c.CurrentUser.ID},
|
OwnerIDs: []int{c.CurrentUser.ID},
|
||||||
|
@ -521,7 +540,19 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
||||||
SubmitUrl: hmnurl.BuildSnippetSubmit(),
|
SubmitUrl: hmnurl.BuildSnippetSubmit(),
|
||||||
AssetMaxSize: AssetMaxSize(c.CurrentUser),
|
AssetMaxSize: AssetMaxSize(c.CurrentUser),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
followUrl = hmnurl.BuildFollowProject()
|
||||||
|
following, err = db.QueryOneScalar[bool](c, c.Conn, `
|
||||||
|
SELECT COUNT(*) > 0
|
||||||
|
FROM follower
|
||||||
|
WHERE user_id = $1 AND following_project_id = $2
|
||||||
|
`, c.CurrentUser.ID, c.CurrentProject.ID)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch following status"))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
templateData.FollowUrl = followUrl
|
||||||
|
templateData.Following = following
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
err = res.WriteTemplate("project_homepage.html", templateData, c.Perf)
|
err = res.WriteTemplate("project_homepage.html", templateData, c.Perf)
|
||||||
|
|
|
@ -165,6 +165,10 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
||||||
hmnOnly.GET(hmnurl.RegexSnippet, Snippet)
|
hmnOnly.GET(hmnurl.RegexSnippet, Snippet)
|
||||||
hmnOnly.GET(hmnurl.RegexProjectIndex, ProjectIndex)
|
hmnOnly.GET(hmnurl.RegexProjectIndex, ProjectIndex)
|
||||||
|
|
||||||
|
hmnOnly.GET(hmnurl.RegexFollowingTest, needsAuth(FollowingTest))
|
||||||
|
hmnOnly.POST(hmnurl.RegexFollowUser, needsAuth(csrfMiddleware(FollowUser)))
|
||||||
|
hmnOnly.POST(hmnurl.RegexFollowProject, needsAuth(csrfMiddleware(FollowProject)))
|
||||||
|
|
||||||
hmnOnly.GET(hmnurl.RegexProjectNew, needsAuth(ProjectNew))
|
hmnOnly.GET(hmnurl.RegexProjectNew, needsAuth(ProjectNew))
|
||||||
hmnOnly.POST(hmnurl.RegexProjectNew, needsAuth(csrfMiddleware(ProjectNewSubmit)))
|
hmnOnly.POST(hmnurl.RegexProjectNew, needsAuth(csrfMiddleware(ProjectNewSubmit)))
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,9 @@ type UserProfileTemplateData struct {
|
||||||
CanAddProject bool
|
CanAddProject bool
|
||||||
NewProjectUrl string
|
NewProjectUrl string
|
||||||
|
|
||||||
|
FollowUrl string
|
||||||
|
Following bool
|
||||||
|
|
||||||
AdminSetOptionsUrl string
|
AdminSetOptionsUrl string
|
||||||
AdminNukeUrl string
|
AdminNukeUrl string
|
||||||
|
|
||||||
|
@ -113,12 +116,10 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
|
||||||
posts, err := hmndata.FetchPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
|
posts, err := hmndata.FetchPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
|
||||||
UserIDs: []int{profileUser.ID},
|
UserIDs: []int{profileUser.ID},
|
||||||
SortDescending: true,
|
SortDescending: true,
|
||||||
})
|
})
|
||||||
c.Perf.EndBlock()
|
|
||||||
|
|
||||||
snippets, err := hmndata.FetchSnippets(c, c.Conn, c.CurrentUser, hmndata.SnippetQuery{
|
snippets, err := hmndata.FetchSnippets(c, c.Conn, c.CurrentUser, hmndata.SnippetQuery{
|
||||||
OwnerIDs: []int{profileUser.ID},
|
OwnerIDs: []int{profileUser.ID},
|
||||||
|
@ -172,6 +173,9 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
baseData := getBaseDataAutocrumb(c, templateUser.Name)
|
baseData := getBaseDataAutocrumb(c, templateUser.Name)
|
||||||
|
|
||||||
|
ownProfile := (c.CurrentUser != nil && c.CurrentUser.ID == profileUser.ID)
|
||||||
|
followUrl := ""
|
||||||
|
following := false
|
||||||
snippetEdit := templates.SnippetEdit{}
|
snippetEdit := templates.SnippetEdit{}
|
||||||
if c.CurrentUser != nil {
|
if c.CurrentUser != nil {
|
||||||
snippetEdit = templates.SnippetEdit{
|
snippetEdit = templates.SnippetEdit{
|
||||||
|
@ -179,6 +183,19 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
SubmitUrl: hmnurl.BuildSnippetSubmit(),
|
SubmitUrl: hmnurl.BuildSnippetSubmit(),
|
||||||
AssetMaxSize: AssetMaxSize(c.CurrentUser),
|
AssetMaxSize: AssetMaxSize(c.CurrentUser),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ownProfile {
|
||||||
|
followUrl = hmnurl.BuildFollowUser()
|
||||||
|
following, err = db.QueryOneScalar[bool](c, c.Conn, `
|
||||||
|
SELECT COUNT(*) > 0
|
||||||
|
FROM follower
|
||||||
|
WHERE user_id = $1 AND following_user_id = $2
|
||||||
|
`, c.CurrentUser.ID, profileUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch following status"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
|
@ -188,11 +205,14 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
ProfileUserLinks: profileUserLinks,
|
ProfileUserLinks: profileUserLinks,
|
||||||
ProfileUserProjects: templateProjects,
|
ProfileUserProjects: templateProjects,
|
||||||
TimelineItems: timelineItems,
|
TimelineItems: timelineItems,
|
||||||
OwnProfile: (c.CurrentUser != nil && c.CurrentUser.ID == profileUser.ID),
|
OwnProfile: ownProfile,
|
||||||
|
|
||||||
CanAddProject: numPersonalProjects < maxPersonalProjects,
|
CanAddProject: numPersonalProjects < maxPersonalProjects,
|
||||||
NewProjectUrl: hmnurl.BuildProjectNew(),
|
NewProjectUrl: hmnurl.BuildProjectNew(),
|
||||||
|
|
||||||
|
FollowUrl: followUrl,
|
||||||
|
Following: following,
|
||||||
|
|
||||||
AdminSetOptionsUrl: hmnurl.BuildAdminSetUserOptions(),
|
AdminSetOptionsUrl: hmnurl.BuildAdminSetUserOptions(),
|
||||||
AdminNukeUrl: hmnurl.BuildAdminNukeUser(),
|
AdminNukeUrl: hmnurl.BuildAdminNukeUser(),
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue