back interdiff of round #1 and #0

appview/pages: header and footer occupy full page width #632

As discussed on Discord, the header and footer now take up full width. I went with the version where the content is still capped at 1024px, like the main content. The changes are purely CSS, except for an extra div around the main content. This is needed because the grid no longer adds a minimum height to the main content, which means the footer will not be pushed to the bottom on pages with little main content. So now instead the header, content and footer are in a flex column, and the content flex-grow’s to make sure it’s at least taking up the remaining viewport space. A few redundant classes have been removed, e.g. grid properties on elements that were not grid-items. I also removed (unused/invisible) border radius and drop-shadow from the header and footer. I tried best possible to check the layout across the different views. There does not currently seem to be any specific UI test suite or similar - let me know if I missed it. Normally I would add screenshots to a PR like this, but this does not seem supported currently. I can share over Discord if you’re interested.

files
appview
docs
spindle
knotserver
config
nix
REVERTED
appview/pages/templates/layouts/fragments/footer.html
···
{{ define "layouts/fragments/footer" }}
+
<div class="w-full p-4 md:p-8 bg-white dark:bg-gray-800 rounded-t drop-shadow-sm">
+
<div class="container mx-auto max-w-7xl px-4">
-
<div class="w-full p-8">
-
<div class="max-w-screen-lg mx-auto px-4">
<div class="flex flex-col lg:flex-row justify-between items-start text-gray-600 dark:text-gray-400 text-sm gap-8">
<div class="mb-4 md:mb-0">
<a href="/" hx-boost="true" class="flex gap-2 font-semibold italic no-underline hover:no-underline">
ERROR
appview/pages/templates/layouts/fragments/topbar.html

Failed to calculate interdiff for this file.

NEW
appview/config/config.go
···
}
type Cloudflare struct {
-
ApiToken string `env:"API_TOKEN"`
-
ZoneId string `env:"ZONE_ID"`
+
ApiToken string `env:"API_TOKEN"`
+
ZoneId string `env:"ZONE_ID"`
+
TurnstileSiteKey string `env:"TURNSTILE_SITE_KEY"`
+
TurnstileSecretKey string `env:"TURNSTILE_SECRET_KEY"`
}
func (cfg RedisConfig) ToURL() string {
NEW
appview/db/email.go
···
return did, nil
}
-
func GetEmailToDid(e Execer, ems []string, isVerifiedFilter bool) (map[string]string, error) {
-
if len(ems) == 0 {
+
func GetEmailToDid(e Execer, emails []string, isVerifiedFilter bool) (map[string]string, error) {
+
if len(emails) == 0 {
return make(map[string]string), nil
}
···
verifiedFilter = 1
}
+
assoc := make(map[string]string)
+
// Create placeholders for the IN clause
-
placeholders := make([]string, len(ems))
-
args := make([]any, len(ems)+1)
+
placeholders := make([]string, 0, len(emails))
+
args := make([]any, 1, len(emails)+1)
args[0] = verifiedFilter
-
for i, em := range ems {
-
placeholders[i] = "?"
-
args[i+1] = em
+
for _, email := range emails {
+
if strings.HasPrefix(email, "did:") {
+
assoc[email] = email
+
continue
+
}
+
placeholders = append(placeholders, "?")
+
args = append(args, email)
}
query := `
···
}
defer rows.Close()
-
assoc := make(map[string]string)
-
for rows.Next() {
var email, did string
if err := rows.Scan(&email, &did); err != nil {
NEW
appview/pages/templates/repo/fork.html
···
</div>
<div class="p-6 bg-white dark:bg-gray-800 drop-shadow-sm rounded">
<form hx-post="/{{ .RepoInfo.FullName }}/fork" class="space-y-12" hx-swap="none" hx-indicator="#spinner">
+
+
<fieldset class="space-y-3">
+
<legend for="repo_name" class="dark:text-white">Repository name</legend>
+
<input type="text" id="repo_name" name="repo_name" value="{{ .RepoInfo.Name }}"
+
class="w-full p-2 border rounded bg-gray-100 dark:bg-gray-700 dark:text-white dark:border-gray-600" />
+
</fieldset>
+
<fieldset class="space-y-3">
<legend class="dark:text-white">Select a knot to fork into</legend>
<div class="space-y-2">
NEW
appview/repo/repo.go
···
// choose a name for a fork
-
forkName := f.Name
+
forkName := r.FormValue("repo_name")
+
if forkName == "" {
+
rp.pages.Notice(w, "repo", "Repository name cannot be empty.")
+
return
+
}
+
// this check is *only* to see if the forked repo name already exists
// in the user's account.
existingRepo, err := db.GetRepo(
rp.db,
db.FilterEq("did", user.Did),
-
db.FilterEq("name", f.Name),
+
db.FilterEq("name", forkName),
if err != nil {
-
if errors.Is(err, sql.ErrNoRows) {
-
// no existing repo with this name found, we can use the name as is
-
} else {
+
if !errors.Is(err, sql.ErrNoRows) {
log.Println("error fetching existing repo from db", "err", err)
rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
return
} else if existingRepo != nil {
-
// repo with this name already exists, append random string
-
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
+
// repo with this name already exists
+
rp.pages.Notice(w, "repo", "A repository with this name already exists.")
+
return
l = l.With("forkName", forkName)
NEW
appview/db/db.go
···
return err
})
+
// add generated at_uri column to pulls table
+
//
+
// this requires a full table recreation because stored columns
+
// cannot be added via alter
+
//
+
// disable foreign-keys for the next migration
+
conn.ExecContext(ctx, "pragma foreign_keys = off;")
+
runMigration(conn, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
+
_, err := tx.Exec(`
+
create table if not exists pulls_new (
+
-- identifiers
+
id integer primary key autoincrement,
+
pull_id integer not null,
+
at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored,
+
+
-- at identifiers
+
repo_at text not null,
+
owner_did text not null,
+
rkey text not null,
+
+
-- content
+
title text not null,
+
body text not null,
+
target_branch text not null,
+
state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted
+
+
-- source info
+
source_branch text,
+
source_repo_at text,
+
+
-- stacking
+
stack_id text,
+
change_id text,
+
parent_change_id text,
+
+
-- meta
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
+
-- constraints
+
unique(repo_at, pull_id),
+
unique(at_uri),
+
foreign key (repo_at) references repos(at_uri) on delete cascade
+
);
+
`)
+
if err != nil {
+
return err
+
}
+
+
// transfer data
+
_, err = tx.Exec(`
+
insert into pulls_new (
+
id, pull_id, repo_at, owner_did, rkey,
+
title, body, target_branch, state,
+
source_branch, source_repo_at,
+
stack_id, change_id, parent_change_id,
+
created
+
)
+
select
+
id, pull_id, repo_at, owner_did, rkey,
+
title, body, target_branch, state,
+
source_branch, source_repo_at,
+
stack_id, change_id, parent_change_id,
+
created
+
from pulls;
+
`)
+
if err != nil {
+
return err
+
}
+
+
// drop old table
+
_, err = tx.Exec(`drop table pulls`)
+
if err != nil {
+
return err
+
}
+
+
// rename new table
+
_, err = tx.Exec(`alter table pulls_new rename to pulls`)
+
return err
+
})
+
conn.ExecContext(ctx, "pragma foreign_keys = on;")
+
+
// remove repo_at and pull_id from pull_submissions and replace with pull_at
+
//
+
// this requires a full table recreation because stored columns
+
// cannot be added via alter
+
//
+
// disable foreign-keys for the next migration
+
conn.ExecContext(ctx, "pragma foreign_keys = off;")
+
runMigration(conn, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
+
_, err := tx.Exec(`
+
create table if not exists pull_submissions_new (
+
-- identifiers
+
id integer primary key autoincrement,
+
pull_at text not null,
+
+
-- content, these are immutable, and require a resubmission to update
+
round_number integer not null default 0,
+
patch text,
+
source_rev text,
+
+
-- meta
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
+
-- constraints
+
unique(pull_at, round_number),
+
foreign key (pull_at) references pulls(at_uri) on delete cascade
+
);
+
`)
+
if err != nil {
+
return err
+
}
+
+
// transfer data, constructing pull_at from pulls table
+
_, err = tx.Exec(`
+
insert into pull_submissions_new (id, pull_at, round_number, patch, created)
+
select
+
ps.id,
+
'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey,
+
ps.round_number,
+
ps.patch,
+
ps.created
+
from pull_submissions ps
+
join pulls p on ps.repo_at = p.repo_at and ps.pull_id = p.pull_id;
+
`)
+
if err != nil {
+
return err
+
}
+
+
// drop old table
+
_, err = tx.Exec(`drop table pull_submissions`)
+
if err != nil {
+
return err
+
}
+
+
// rename new table
+
_, err = tx.Exec(`alter table pull_submissions_new rename to pull_submissions`)
+
return err
+
})
+
conn.ExecContext(ctx, "pragma foreign_keys = on;")
+
return &DB{db}, nil
}
NEW
appview/issues/issues.go
···
return
}
-
labelDefs, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
labelDefs, err := db.GetLabelDefinitions(
+
rp.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterContains("scope", tangled.RepoIssueNSID),
+
)
if err != nil {
log.Println("failed to fetch labels", err)
rp.pages.Error503(w)
NEW
appview/pages/templates/repo/fragments/labelPanel.html
···
{{ define "repo/fragments/labelPanel" }}
-
<div id="label-panel" class="flex flex-col gap-6">
+
<div id="label-panel" class="flex flex-col gap-6 px-6 md:px-0">
{{ template "basicLabels" . }}
{{ template "kvLabels" . }}
</div>
NEW
appview/pages/templates/repo/fragments/participants.html
···
+
{{ define "repo/fragments/participants" }}
+
{{ $all := . }}
+
{{ $ps := take $all 5 }}
+
<div class="px-6 md:px-0">
+
<div class="py-1 flex items-center text-sm">
+
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
+
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
+
</div>
+
<div class="flex items-center -space-x-3 mt-2">
+
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
+
{{ range $i, $p := $ps }}
+
<img
+
src="{{ tinyAvatar . }}"
+
alt=""
+
class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0"
+
/>
+
{{ end }}
+
+
{{ if gt (len $all) 5 }}
+
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
+
+{{ sub (len $all) 5 }}
+
</span>
+
{{ end }}
+
</div>
+
</div>
+
{{ end }}
NEW
appview/pages/templates/repo/issues/issue.html
···
"Defs" $.LabelDefs
"Subject" $.Issue.AtUri
"State" $.Issue.Labels) }}
-
{{ template "issueParticipants" . }}
+
{{ template "repo/fragments/participants" $.Issue.Participants }}
</div>
</div>
{{ end }}
···
</div>
{{ end }}
-
{{ define "issueParticipants" }}
-
{{ $all := .Issue.Participants }}
-
{{ $ps := take $all 5 }}
-
<div>
-
<div class="py-1 flex items-center text-sm">
-
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
-
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
-
</div>
-
<div class="flex items-center -space-x-3 mt-2">
-
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
-
{{ range $i, $p := $ps }}
-
<img
-
src="{{ tinyAvatar . }}"
-
alt=""
-
class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0"
-
/>
-
{{ end }}
-
-
{{ if gt (len $all) 5 }}
-
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
-
+{{ sub (len $all) 5 }}
-
</span>
-
{{ end }}
-
</div>
-
</div>
-
{{ end }}
{{ define "repoAfter" }}
<div class="flex flex-col gap-4 mt-4">
NEW
appview/pages/templates/repo/pulls/pull.html
···
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
{{ end }}
+
{{ define "repoContentLayout" }}
+
<div class="grid grid-cols-1 md:grid-cols-10 gap-4 w-full">
+
<div class="col-span-1 md:col-span-8">
+
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white">
+
{{ block "repoContent" . }}{{ end }}
+
</section>
+
{{ block "repoAfter" . }}{{ end }}
+
</div>
+
<div class="col-span-1 md:col-span-2 flex flex-col gap-6">
+
{{ template "repo/fragments/labelPanel"
+
(dict "RepoInfo" $.RepoInfo
+
"Defs" $.LabelDefs
+
"Subject" $.Pull.PullAt
+
"State" $.Pull.Labels) }}
+
{{ template "repo/fragments/participants" $.Pull.Participants }}
+
</div>
+
</div>
+
{{ end }}
{{ define "repoContent" }}
{{ template "repo/pulls/fragments/pullHeader" . }}
···
{{ with $item }}
<details {{ if eq $idx $lastIdx }}open{{ end }}>
<summary id="round-#{{ .RoundNumber }}" class="list-none cursor-pointer">
-
<div class="flex flex-wrap gap-2 items-center">
+
<div class="flex flex-wrap gap-2 items-stretch">
<!-- round number -->
<div class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-3 py-2 dark:text-white">
<span class="flex items-center">{{ i "hash" "w-4 h-4" }}{{ .RoundNumber }}</span>
</div>
<!-- round summary -->
-
<div class="rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400">
+
<div class="flex-1 rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400">
<span class="gap-1 flex items-center">
{{ $owner := resolve $.Pull.OwnerDid }}
{{ $re := "re" }}
···
<span class="hidden md:inline">diff</span>
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
</a>
-
{{ if not (eq .RoundNumber 0) }}
-
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
-
hx-boost="true"
-
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
-
{{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }}
-
<span class="hidden md:inline">interdiff</span>
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</a>
-
<span id="interdiff-error-{{.RoundNumber}}"></span>
+
{{ if ne $idx 0 }}
+
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
+
hx-boost="true"
+
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
+
{{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }}
+
<span class="hidden md:inline">interdiff</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</a>
{{ end }}
+
<span id="interdiff-error-{{.RoundNumber}}"></span>
</div>
</summary>
···
<div class="md:pl-[3.5rem] flex flex-col gap-2 mt-2 relative">
{{ range $cidx, $c := .Comments }}
-
<div id="comment-{{$c.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full md:max-w-3/5 md:w-fit">
+
<div id="comment-{{$c.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full">
{{ if gt $cidx 0 }}
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
{{ end }}
NEW
appview/pages/templates/repo/pulls/pulls.html
···
<span class="before:content-['·']"></span>
{{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }}
{{ end }}
+
+
{{ $state := .Labels }}
+
{{ range $k, $d := $.LabelDefs }}
+
{{ range $v, $s := $state.GetValSet $d.AtUri.String }}
+
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
+
{{ end }}
+
{{ end }}
</div>
</div>
{{ if .StackId }}
NEW
appview/pulls/pulls.go
···
userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.PullAt())
}
+
labelDefs, err := db.GetLabelDefinitions(
+
s.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterContains("scope", tangled.RepoPullNSID),
+
)
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
s.pages.Error503(w)
+
return
+
}
+
+
defs := make(map[string]*models.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
LoggedInUser: user,
RepoInfo: repoInfo,
···
OrderedReactionKinds: models.OrderedReactionKinds,
Reactions: reactionCountMap,
UserReacted: userReactions,
+
+
LabelDefs: defs,
})
}
···
m[p.Sha] = p
}
+
labelDefs, err := db.GetLabelDefinitions(
+
s.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterContains("scope", tangled.RepoPullNSID),
+
)
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
s.pages.Error503(w)
+
return
+
}
+
+
defs := make(map[string]*models.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
s.pages.RepoPulls(w, pages.RepoPullsParams{
LoggedInUser: s.oauth.GetUser(r),
RepoInfo: f.RepoInfo(user),
Pulls: pulls,
+
LabelDefs: defs,
FilteringBy: state,
Stacks: stacks,
Pipelines: m,
NEW
appview/notify/db/db.go
···
func (n *databaseNotifier) NewStar(ctx context.Context, star *models.Star) {
var err error
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(star.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt)))
if err != nil {
log.Printf("NewStar: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewStar: no repo found for %s", star.RepoAt)
-
return
-
}
-
repo := repos[0]
// don't notify yourself
if repo.Did == star.StarredByDid {
···
}
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
if err != nil {
log.Printf("NewIssue: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewIssue: no repo found for %s", issue.RepoAt)
-
return
-
}
-
repo := repos[0]
if repo.Did == issue.Did {
return
···
}
issue := issues[0]
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
if err != nil {
log.Printf("NewIssueComment: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewIssueComment: no repo found for %s", issue.RepoAt)
-
return
-
}
-
repo := repos[0]
recipients := make(map[string]bool)
···
}
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
if err != nil {
log.Printf("NewPull: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewPull: no repo found for %s", pull.RepoAt)
-
return
-
}
-
repo := repos[0]
if repo.Did == pull.OwnerDid {
return
···
}
pull := pulls[0]
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", comment.RepoAt))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
if err != nil {
log.Printf("NewPullComment: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewPullComment: no repo found for %s", comment.RepoAt)
-
return
-
}
-
repo := repos[0]
recipients := make(map[string]bool)
···
func (n *databaseNotifier) NewIssueClosed(ctx context.Context, issue *models.Issue) {
// Get repo details
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
if err != nil {
log.Printf("NewIssueClosed: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewIssueClosed: no repo found for %s", issue.RepoAt)
-
return
-
}
-
repo := repos[0]
// Don't notify yourself
if repo.Did == issue.Did {
···
func (n *databaseNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
// Get repo details
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
if err != nil {
log.Printf("NewPullMerged: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewPullMerged: no repo found for %s", pull.RepoAt)
-
return
-
}
-
repo := repos[0]
// Don't notify yourself
if repo.Did == pull.OwnerDid {
···
func (n *databaseNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
// Get repo details
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
if err != nil {
log.Printf("NewPullClosed: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewPullClosed: no repo found for %s", pull.RepoAt)
-
return
-
}
-
repo := repos[0]
// Don't notify yourself
if repo.Did == pull.OwnerDid {
NEW
appview/models/notifications.go
···
package models
-
import "time"
+
import (
+
"time"
+
)
type NotificationType string
···
PullId *int64
}
+
// lucide icon that represents this notification
+
func (n *Notification) Icon() string {
+
switch n.Type {
+
case NotificationTypeRepoStarred:
+
return "star"
+
case NotificationTypeIssueCreated:
+
return "circle-dot"
+
case NotificationTypeIssueCommented:
+
return "message-square"
+
case NotificationTypeIssueClosed:
+
return "ban"
+
case NotificationTypePullCreated:
+
return "git-pull-request-create"
+
case NotificationTypePullCommented:
+
return "message-square"
+
case NotificationTypePullMerged:
+
return "git-merge"
+
case NotificationTypePullClosed:
+
return "git-pull-request-closed"
+
case NotificationTypeFollowed:
+
return "user-plus"
+
default:
+
return ""
+
}
+
}
+
type NotificationWithEntity struct {
*Notification
Repo *Repo
NEW
appview/pages/pages.go
···
LoggedInUser *oauth.User
Notifications []*models.NotificationWithEntity
UnreadCount int
-
HasMore bool
-
NextOffset int
-
Limit int
+
Page pagination.Page
+
Total int64
}
func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error {
···
}
type NotificationCountParams struct {
-
Count int
+
Count int64
}
func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error {
NEW
appview/pagination/page.go
···
func FirstPage() Page {
return Page{
Offset: 0,
-
Limit: 10,
+
Limit: 30,
}
}
NEW
nix/pkgs/knot-unwrapped.nix
···
sqlite-lib,
src,
}: let
-
version = "1.9.0-alpha";
+
version = "1.9.1-alpha";
in
buildGoApplication {
pname = "knot";
NEW
appview/pages/templates/repo/fragments/cloneDropdown.html
···
{{ define "repo/fragments/cloneDropdown" }}
{{ $knot := .RepoInfo.Knot }}
{{ if eq $knot "knot1.tangled.sh" }}
-
{{ $knot = "tangled.sh" }}
+
{{ $knot = "tangled.org" }}
{{ end }}
<details id="clone-dropdown" class="relative inline-block text-left group">
NEW
docs/spindle/pipeline.md
···
- `manual`: The workflow can be triggered manually.
- `branch`: This is a **required** field that defines which branches the workflow should run for. If used with the `push` event, commits to the branch(es) listed here will trigger the workflow. If used with the `pull_request` event, updates to pull requests targeting the branch(es) listed here will trigger the workflow. This field has no effect with the `manual` event.
-
For example, if you'd like define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with:
+
For example, if you'd like to define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with:
```yaml
when:
NEW
knotserver/config/config.go
···
Repo Repo `env:",prefix=KNOT_REPO_"`
Server Server `env:",prefix=KNOT_SERVER_"`
Git Git `env:",prefix=KNOT_GIT_"`
-
AppViewEndpoint string `env:"APPVIEW_ENDPOINT, default=https://tangled.sh"`
+
AppViewEndpoint string `env:"APPVIEW_ENDPOINT, default=https://tangled.org"`
}
func Load(ctx context.Context) (*Config, error) {
NEW
appview/pages/templates/user/completeSignup.html
···
content="complete your signup for tangled"
/>
<script src="/static/htmx.min.js"></script>
+
<link rel="manifest" href="/pwa-manifest.json" />
<link
rel="stylesheet"
href="/static/tw.css?{{ cssContentHash }}"
NEW
appview/pages/templates/user/signup.html
···
<meta property="og:url" content="https://tangled.org/signup" />
<meta property="og:description" content="sign up for tangled" />
<script src="/static/htmx.min.js"></script>
+
<link rel="manifest" href="/pwa-manifest.json" />
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
<title>sign up &middot; tangled</title>
NEW
appview/state/router.go
···
router.Use(middleware.TryRefreshSession())
router.Get("/favicon.svg", s.Favicon)
router.Get("/favicon.ico", s.Favicon)
+
router.Get("/pwa-manifest.json", s.PWAManifest)
userRouter := s.UserRouter(&middleware)
standardRouter := s.StandardRouter(&middleware)
NEW
appview/state/state.go
···
s.pages.Favicon(w)
}
+
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
+
const manifestJson = `{
+
"name": "tangled",
+
"description": "tightly-knit social coding.",
+
"icons": [
+
{
+
"src": "/favicon.svg",
+
"sizes": "144x144"
+
}
+
],
+
"start_url": "/",
+
"id": "org.tangled",
+
+
"display": "standalone",
+
"background_color": "#111827",
+
"theme_color": "#111827"
+
}`
+
+
func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) {
+
w.Header().Set("Content-Type", "application/json")
+
w.Write([]byte(manifestJson))
+
}
+
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
user := s.oauth.GetUser(r)
s.pages.TermsOfService(w, pages.TermsOfServiceParams{