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.Tracer = &tracelog.TraceLog{
|
||||
zerologadapter.NewLogger(log.Logger),
|
||||
cfg.LogLevel,
|
||||
Logger: zerologadapter.NewLogger(log.Logger),
|
||||
LogLevel: cfg.LogLevel,
|
||||
}
|
||||
|
||||
conn, err := pgx.ConnectConfig(context.Background(), pgcfg)
|
||||
|
@ -88,8 +88,8 @@ func NewConnPoolWithConfig(cfg config.PostgresConfig) *pgxpool.Pool {
|
|||
pgcfg.MinConns = cfg.MinConn
|
||||
pgcfg.MaxConns = cfg.MaxConn
|
||||
pgcfg.ConnConfig.Tracer = &tracelog.TraceLog{
|
||||
zerologadapter.NewLogger(log.Logger),
|
||||
cfg.LogLevel,
|
||||
Logger: zerologadapter.NewLogger(log.Logger),
|
||||
LogLevel: cfg.LogLevel,
|
||||
}
|
||||
|
||||
conn, err := pgxpool.NewWithConfig(context.Background(), pgcfg)
|
||||
|
|
|
@ -937,6 +937,28 @@ func BuildTwitchEventSubCallback() string {
|
|||
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
</div>
|
||||
{{ 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) }}
|
||||
<hr class="w-100 mv4">
|
||||
<div class="w-100 flex g3">
|
||||
|
|
|
@ -43,6 +43,34 @@
|
|||
{{ with or .ProfileUser.Bio .ProfileUser.Blurb }}
|
||||
<div class="mb3">{{ . }}</div>
|
||||
{{ 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">
|
||||
{{ if .ProfileUser.Email }}
|
||||
<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 {
|
||||
maxRecentActivity := 15
|
||||
|
||||
|
@ -389,6 +403,9 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
|||
NamedLinks, UnnamedLinks []templates.Link
|
||||
RecentActivity []templates.TimelineItem
|
||||
SnippetEdit templates.SnippetEdit
|
||||
|
||||
FollowUrl string
|
||||
Following bool
|
||||
}
|
||||
|
||||
var templateData ProjectHomepageData
|
||||
|
@ -500,6 +517,8 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
|||
})
|
||||
c.Perf.EndBlock()
|
||||
|
||||
followUrl := ""
|
||||
following := false
|
||||
if c.CurrentUser != nil {
|
||||
userProjects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
|
||||
OwnerIDs: []int{c.CurrentUser.ID},
|
||||
|
@ -521,7 +540,19 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
|||
SubmitUrl: hmnurl.BuildSnippetSubmit(),
|
||||
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
|
||||
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.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.POST(hmnurl.RegexProjectNew, needsAuth(csrfMiddleware(ProjectNewSubmit)))
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@ type UserProfileTemplateData struct {
|
|||
CanAddProject bool
|
||||
NewProjectUrl string
|
||||
|
||||
FollowUrl string
|
||||
Following bool
|
||||
|
||||
AdminSetOptionsUrl string
|
||||
AdminNukeUrl string
|
||||
|
||||
|
@ -113,12 +116,10 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
}
|
||||
c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||
posts, err := hmndata.FetchPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
|
||||
UserIDs: []int{profileUser.ID},
|
||||
SortDescending: true,
|
||||
})
|
||||
c.Perf.EndBlock()
|
||||
|
||||
snippets, err := hmndata.FetchSnippets(c, c.Conn, c.CurrentUser, hmndata.SnippetQuery{
|
||||
OwnerIDs: []int{profileUser.ID},
|
||||
|
@ -172,6 +173,9 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
|
||||
baseData := getBaseDataAutocrumb(c, templateUser.Name)
|
||||
|
||||
ownProfile := (c.CurrentUser != nil && c.CurrentUser.ID == profileUser.ID)
|
||||
followUrl := ""
|
||||
following := false
|
||||
snippetEdit := templates.SnippetEdit{}
|
||||
if c.CurrentUser != nil {
|
||||
snippetEdit = templates.SnippetEdit{
|
||||
|
@ -179,6 +183,19 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
SubmitUrl: hmnurl.BuildSnippetSubmit(),
|
||||
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
|
||||
|
@ -188,11 +205,14 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
ProfileUserLinks: profileUserLinks,
|
||||
ProfileUserProjects: templateProjects,
|
||||
TimelineItems: timelineItems,
|
||||
OwnProfile: (c.CurrentUser != nil && c.CurrentUser.ID == profileUser.ID),
|
||||
OwnProfile: ownProfile,
|
||||
|
||||
CanAddProject: numPersonalProjects < maxPersonalProjects,
|
||||
NewProjectUrl: hmnurl.BuildProjectNew(),
|
||||
|
||||
FollowUrl: followUrl,
|
||||
Following: following,
|
||||
|
||||
AdminSetOptionsUrl: hmnurl.BuildAdminSetUserOptions(),
|
||||
AdminNukeUrl: hmnurl.BuildAdminNukeUser(),
|
||||
|
||||
|
|
Loading…
Reference in New Issue