forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+222 -285
appview
db
notifications
pages
templates
layouts
notifications
fragments
repo
fragments
user
pagination
repo
signup
state
docs
spindle
knotserver
config
nix
+18 -13
appview/db/notifications.go
···
import (
"context"
"database/sql"
+
"errors"
"fmt"
+
"strings"
"time"
"tangled.org/core/appview/models"
···
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
}
-
func (d *DB) GetUnreadNotificationCount(ctx context.Context, userDID string) (int, error) {
-
recipientFilter := FilterEq("recipient_did", userDID)
-
readFilter := FilterEq("read", 0)
+
func CountNotifications(e Execer, filters ...filter) (int64, error) {
+
var conditions []string
+
var args []any
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
-
query := fmt.Sprintf(`
-
SELECT COUNT(*)
-
FROM notifications
-
WHERE %s AND %s
-
`, recipientFilter.Condition(), readFilter.Condition())
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
-
args := append(recipientFilter.Arg(), readFilter.Arg()...)
+
query := fmt.Sprintf(`select count(1) from notifications %s`, whereClause)
+
var count int64
+
err := e.QueryRow(query, args...).Scan(&count)
-
var count int
-
err := d.DB.QueryRowContext(ctx, query, args...).Scan(&count)
-
if err != nil {
-
return 0, fmt.Errorf("failed to get unread count: %w", err)
+
if !errors.Is(err, sql.ErrNoRows) && err != nil {
+
return 0, err
}
return count, nil
+29 -36
appview/notifications/notifications.go
···
package notifications
import (
+
"fmt"
"log"
"net/http"
"strconv"
···
r.Use(middleware.AuthMiddleware(n.oauth))
-
r.Get("/", n.notificationsPage)
+
r.With(middleware.Paginate).Get("/", n.notificationsPage)
r.Get("/count", n.getUnreadCount)
r.Post("/{id}/read", n.markRead)
···
func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) {
userDid := n.oauth.GetDid(r)
-
limitStr := r.URL.Query().Get("limit")
-
offsetStr := r.URL.Query().Get("offset")
-
-
limit := 20 // default
-
if limitStr != "" {
-
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
-
limit = l
-
}
+
page, ok := r.Context().Value("page").(pagination.Page)
+
if !ok {
+
log.Println("failed to get page")
+
page = pagination.FirstPage()
}
-
offset := 0 // default
-
if offsetStr != "" {
-
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
-
offset = o
-
}
+
total, err := db.CountNotifications(
+
n.db,
+
db.FilterEq("recipient_did", userDid),
+
)
+
if err != nil {
+
log.Println("failed to get total notifications:", err)
+
n.pages.Error500(w)
+
return
}
-
page := pagination.Page{Limit: limit + 1, Offset: offset}
-
notifications, err := db.GetNotificationsWithEntities(n.db, page, db.FilterEq("recipient_did", userDid))
+
notifications, err := db.GetNotificationsWithEntities(
+
n.db,
+
page,
+
db.FilterEq("recipient_did", userDid),
+
)
if err != nil {
log.Println("failed to get notifications:", err)
n.pages.Error500(w)
return
}
-
hasMore := len(notifications) > limit
-
if hasMore {
-
notifications = notifications[:limit]
-
}
-
err = n.db.MarkAllNotificationsRead(r.Context(), userDid)
if err != nil {
log.Println("failed to mark notifications as read:", err)
···
return
}
-
params := pages.NotificationsParams{
+
fmt.Println(n.pages.Notifications(w, pages.NotificationsParams{
LoggedInUser: user,
Notifications: notifications,
UnreadCount: unreadCount,
-
HasMore: hasMore,
-
NextOffset: offset + limit,
-
Limit: limit,
-
}
-
-
err = n.pages.Notifications(w, params)
-
if err != nil {
-
log.Println("failed to load notifs:", err)
-
n.pages.Error500(w)
-
return
-
}
+
Page: page,
+
Total: total,
+
}))
}
func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) {
-
userDid := n.oauth.GetDid(r)
-
-
count, err := n.db.GetUnreadNotificationCount(r.Context(), userDid)
+
user := n.oauth.GetUser(r)
+
count, err := db.CountNotifications(
+
n.db,
+
db.FilterEq("recipient_did", user.Did),
+
db.FilterEq("read", 0),
+
)
if err != nil {
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
return
+3 -4
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 {
+48 -15
appview/pages/templates/notifications/list.html
···
</div>
</div>
-
{{if .Notifications}}
-
<div class="flex flex-col gap-2" id="notifications-list">
-
{{range .Notifications}}
-
{{template "notifications/fragments/item" .}}
-
{{end}}
-
</div>
+
{{if .Notifications}}
+
<div class="flex flex-col gap-2" id="notifications-list">
+
{{range .Notifications}}
+
{{template "notifications/fragments/item" .}}
+
{{end}}
+
</div>
-
{{else}}
-
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
-
<div class="text-center py-12">
-
<div class="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600">
-
{{ i "bell-off" "w-16 h-16" }}
-
</div>
-
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No notifications</h3>
-
<p class="text-gray-600 dark:text-gray-400">When you receive notifications, they'll appear here.</p>
+
{{else}}
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
<div class="text-center py-12">
+
<div class="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600">
+
{{ i "bell-off" "w-16 h-16" }}
</div>
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No notifications</h3>
+
<p class="text-gray-600 dark:text-gray-400">When you receive notifications, they'll appear here.</p>
</div>
-
{{end}}
+
</div>
+
{{end}}
+
+
{{ template "pagination" . }}
+
{{ end }}
+
+
{{ define "pagination" }}
+
<div class="flex justify-end mt-4 gap-2">
+
{{ if gt .Page.Offset 0 }}
+
{{ $prev := .Page.Previous }}
+
<a
+
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
+
hx-boost="true"
+
href = "/notifications?offset={{ $prev.Offset }}&limit={{ $prev.Limit }}"
+
>
+
{{ i "chevron-left" "w-4 h-4" }}
+
previous
+
</a>
+
{{ else }}
+
<div></div>
+
{{ end }}
+
+
{{ $next := .Page.Next }}
+
{{ if lt $next.Offset .Total }}
+
{{ $next := .Page.Next }}
+
<a
+
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
+
hx-boost="true"
+
href = "/notifications?offset={{ $next.Offset }}&limit={{ $next.Limit }}"
+
>
+
next
+
{{ i "chevron-right" "w-4 h-4" }}
+
</a>
+
{{ end }}
+
</div>
{{ end }}
+1 -1
appview/pagination/page.go
···
func FirstPage() Page {
return Page{
Offset: 0,
-
Limit: 10,
+
Limit: 30,
}
}
+27 -208
appview/pages/templates/notifications/fragments/item.html
···
{{define "notifications/fragments/item"}}
-
<div
-
class="
-
w-full mx-auto rounded drop-shadow-sm dark:text-white bg-white dark:bg-gray-800 px-2 md:px-6 py-4 transition-colors
-
{{if not .Read}}bg-blue-50 dark:bg-blue-900/20 border border-blue-500 dark:border-sky-800{{end}}
-
flex gap-2 items-center
-
"
-
>
+
<a href="{{ template "notificationUrl" . }}" class="block no-underline hover:no-underline">
+
<div
+
class="
+
w-full mx-auto rounded drop-shadow-sm dark:text-white bg-white dark:bg-gray-800 px-2 md:px-6 py-4 transition-colors
+
{{if not .Read}}bg-blue-50 dark:bg-blue-800/20 border border-blue-500 dark:border-sky-800{{end}}
+
flex gap-2 items-center
+
">
+
{{ template "notificationIcon" . }}
+
<div class="flex-1 w-full flex flex-col gap-1">
+
<span>{{ template "notificationHeader" . }}</span>
+
<span class="text-sm text-gray-500 dark:text-gray-400">{{ template "notificationSummary" . }}</span>
+
</div>
-
{{ template "notificationIcon" . }}
-
<div class="flex-1 w-full flex flex-col gap-1">
-
<span>{{ template "notificationHeader" . }}</span>
-
<span class="text-sm text-gray-500 dark:text-gray-400">{{ template "notificationSummary" . }}</span>
</div>
-
-
</div>
+
</a>
{{end}}
{{ define "notificationIcon" }}
···
{{ end }}
{{ end }}
-
{{define "issueNotification"}}
-
{{$url := printf "/%s/%s/issues/%d" (resolve .Repo.Did) .Repo.Name .Issue.IssueId}}
-
<a
-
href="{{$url}}"
-
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
-
>
-
<div class="flex items-center justify-between">
-
<div class="min-w-0 flex-1">
-
<!-- First line: icon + actor action -->
-
<div class="flex items-center gap-2 text-gray-900 dark:text-white">
-
{{if eq .Type "issue_created"}}
-
<span class="text-green-600 dark:text-green-500">
-
{{ i "circle-dot" "w-4 h-4" }}
-
</span>
-
{{else if eq .Type "issue_commented"}}
-
<span class="text-gray-500 dark:text-gray-400">
-
{{ i "message-circle" "w-4 h-4" }}
-
</span>
-
{{else if eq .Type "issue_closed"}}
-
<span class="text-gray-500 dark:text-gray-400">
-
{{ i "ban" "w-4 h-4" }}
-
</span>
-
{{end}}
-
{{template "user/fragments/picHandle" .ActorDid}}
-
{{if eq .Type "issue_created"}}
-
<span class="text-gray-500 dark:text-gray-400">opened issue</span>
-
{{else if eq .Type "issue_commented"}}
-
<span class="text-gray-500 dark:text-gray-400">commented on issue</span>
-
{{else if eq .Type "issue_closed"}}
-
<span class="text-gray-500 dark:text-gray-400">closed issue</span>
-
{{end}}
-
{{if not .Read}}
-
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
-
{{end}}
-
</div>
-
-
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5 ml-6 flex items-center gap-1">
-
<span class="text-gray-500 dark:text-gray-400">#{{.Issue.IssueId}}</span>
-
<span class="text-gray-900 dark:text-white truncate">{{.Issue.Title}}</span>
-
<span>on</span>
-
<span class="font-medium text-gray-900 dark:text-white">{{resolve .Repo.Did}}/{{.Repo.Name}}</span>
-
</div>
-
</div>
-
-
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
-
{{ template "repo/fragments/time" .Created }}
-
</div>
-
</div>
-
</a>
-
{{end}}
-
-
{{define "pullNotification"}}
-
{{$url := printf "/%s/%s/pulls/%d" (resolve .Repo.Did) .Repo.Name .Pull.PullId}}
-
<a
-
href="{{$url}}"
-
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
-
>
-
<div class="flex items-center justify-between">
-
<div class="min-w-0 flex-1">
-
<div class="flex items-center gap-2 text-gray-900 dark:text-white">
-
{{if eq .Type "pull_created"}}
-
<span class="text-green-600 dark:text-green-500">
-
{{ i "git-pull-request-create" "w-4 h-4" }}
-
</span>
-
{{else if eq .Type "pull_commented"}}
-
<span class="text-gray-500 dark:text-gray-400">
-
{{ i "message-circle" "w-4 h-4" }}
-
</span>
-
{{else if eq .Type "pull_merged"}}
-
<span class="text-purple-600 dark:text-purple-500">
-
{{ i "git-merge" "w-4 h-4" }}
-
</span>
-
{{else if eq .Type "pull_closed"}}
-
<span class="text-red-600 dark:text-red-500">
-
{{ i "git-pull-request-closed" "w-4 h-4" }}
-
</span>
-
{{end}}
-
{{template "user/fragments/picHandle" (resolve .ActorDid)}}
-
{{if eq .Type "pull_created"}}
-
<span class="text-gray-500 dark:text-gray-400">opened pull request</span>
-
{{else if eq .Type "pull_commented"}}
-
<span class="text-gray-500 dark:text-gray-400">commented on pull request</span>
-
{{else if eq .Type "pull_merged"}}
-
<span class="text-gray-500 dark:text-gray-400">merged pull request</span>
-
{{else if eq .Type "pull_closed"}}
-
<span class="text-gray-500 dark:text-gray-400">closed pull request</span>
-
{{end}}
-
{{if not .Read}}
-
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
-
{{end}}
-
</div>
-
-
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5 ml-6 flex items-center gap-1">
-
<span class="text-gray-500 dark:text-gray-400">#{{.Pull.PullId}}</span>
-
<span class="text-gray-900 dark:text-white truncate">{{.Pull.Title}}</span>
-
<span>on</span>
-
<span class="font-medium text-gray-900 dark:text-white">{{resolve .Repo.Did}}/{{.Repo.Name}}</span>
-
</div>
-
</div>
-
-
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
-
{{ template "repo/fragments/time" .Created }}
-
</div>
-
</div>
-
</a>
-
{{end}}
-
-
{{define "repoNotification"}}
-
{{$url := printf "/%s/%s" (resolve .Repo.Did) .Repo.Name}}
-
<a
-
href="{{$url}}"
-
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
-
>
-
<div class="flex items-center justify-between">
-
<div class="flex items-center gap-2 min-w-0 flex-1">
-
<span class="text-yellow-500 dark:text-yellow-400">
-
{{ i "star" "w-4 h-4" }}
-
</span>
-
-
<div class="min-w-0 flex-1">
-
<!-- Single line for stars: actor action subject -->
-
<div class="flex items-center gap-1 text-gray-900 dark:text-white">
-
{{template "user/fragments/picHandle" (resolve .ActorDid)}}
-
<span class="text-gray-500 dark:text-gray-400">starred</span>
-
<span class="font-medium">{{resolve .Repo.Did}}/{{.Repo.Name}}</span>
-
{{if not .Read}}
-
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
-
{{end}}
-
</div>
-
</div>
-
</div>
-
-
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
-
{{ template "repo/fragments/time" .Created }}
-
</div>
-
</div>
-
</a>
-
{{end}}
-
-
{{define "followNotification"}}
-
{{$url := printf "/%s" (resolve .ActorDid)}}
-
<a
-
href="{{$url}}"
-
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
-
>
-
<div class="flex items-center justify-between">
-
<div class="flex items-center gap-2 min-w-0 flex-1">
-
<span class="text-blue-600 dark:text-blue-400">
-
{{ i "user-plus" "w-4 h-4" }}
-
</span>
-
-
<div class="min-w-0 flex-1">
-
<div class="flex items-center gap-1 text-gray-900 dark:text-white">
-
{{template "user/fragments/picHandle" (resolve .ActorDid)}}
-
<span class="text-gray-500 dark:text-gray-400">followed you</span>
-
{{if not .Read}}
-
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
-
{{end}}
-
</div>
-
</div>
-
</div>
-
-
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
-
{{ template "repo/fragments/time" .Created }}
-
</div>
-
</div>
-
</a>
-
{{end}}
-
-
{{define "genericNotification"}}
-
<a
-
href="#"
-
class="block no-underline hover:no-underline text-inherit -m-3 p-3"
-
>
-
<div class="flex items-center justify-between">
-
<div class="flex items-center gap-2 min-w-0 flex-1">
-
<span class="{{if not .Read}}text-blue-600 dark:text-blue-400{{else}}text-gray-500 dark:text-gray-400{{end}}">
-
{{ i "bell" "w-4 h-4" }}
-
</span>
-
-
<div class="min-w-0 flex-1">
-
<div class="flex items-center gap-1 text-gray-900 dark:text-white">
-
<span>New notification</span>
-
{{if not .Read}}
-
<div class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-1"></div>
-
{{end}}
-
</div>
-
</div>
-
</div>
+
{{ define "notificationUrl" }}
+
{{ $url := "" }}
+
{{ if eq .Type "repo_starred" }}
+
{{$url = printf "/%s/%s" (resolve .Repo.Did) .Repo.Name}}
+
{{ else if .Issue }}
+
{{$url = printf "/%s/%s/issues/%d" (resolve .Repo.Did) .Repo.Name .Issue.IssueId}}
+
{{ else if .Pull }}
+
{{$url = printf "/%s/%s/pulls/%d" (resolve .Repo.Did) .Repo.Name .Pull.PullId}}
+
{{ else if eq .Type "followed" }}
+
{{$url = printf "/%s" (resolve .ActorDid)}}
+
{{ else }}
+
{{ end }}
-
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
-
{{ template "repo/fragments/time" .Created }}
-
</div>
-
</div>
-
</a>
-
{{end}}
+
{{ $url }}
+
{{ end }}
+1 -1
nix/pkgs/knot-unwrapped.nix
···
sqlite-lib,
src,
}: let
-
version = "1.9.0-alpha";
+
version = "1.9.1-alpha";
in
buildGoApplication {
pname = "knot";
+1 -1
appview/pages/templates/layouts/fragments/footer.html
···
<a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a>
<a href="https://tangled.org/@tangled.org/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a>
<a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a>
-
<a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a>
+
<a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a>
</div>
<div class="flex flex-col gap-1">
+1 -1
appview/signup/signup.go
···
noticeId := "signup-msg"
if err := s.validateCaptcha(cfToken, r); err != nil {
-
s.l.Warn("turnstile validation failed", "error", err)
+
s.l.Warn("turnstile validation failed", "error", err, "email", emailId)
s.pages.Notice(w, noticeId, "Captcha validation failed.")
return
}
+1 -1
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">
+1 -1
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:
+1 -1
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) {
+3
appview/pages/templates/layouts/base.html
···
<link rel="preconnect" href="https://avatar.tangled.sh" />
<link rel="preconnect" href="https://camo.tangled.sh" />
+
<!-- pwa manifest -->
+
<link rel="manifest" href="/pwa-manifest.json" />
+
<!-- preload main font -->
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
+1
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 }}"
+1
appview/pages/templates/user/login.html
···
<meta property="og:url" content="https://tangled.org/login" />
<meta property="og:description" content="login to 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>login &middot; tangled</title>
</head>
+1
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>
+1
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)
+23
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{
+34
appview/db/language.go
···
package db
import (
+
"database/sql"
"fmt"
"strings"
+
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
)
···
return nil
}
+
+
func DeleteRepoLanguages(e Execer, filters ...filter) error {
+
var conditions []string
+
var args []any
+
for _, filter := range filters {
+
conditions = append(conditions, filter.Condition())
+
args = append(args, filter.Arg()...)
+
}
+
+
whereClause := ""
+
if conditions != nil {
+
whereClause = " where " + strings.Join(conditions, " and ")
+
}
+
+
query := fmt.Sprintf(`delete from repo_languages %s`, whereClause)
+
+
_, err := e.Exec(query, args...)
+
return err
+
}
+
+
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
+
err := DeleteRepoLanguages(
+
tx,
+
FilterEq("repo_at", repoAt),
+
FilterEq("ref", ref),
+
)
+
if err != nil {
+
return fmt.Errorf("failed to delete existing languages: %w", err)
+
}
+
+
return InsertRepoLanguages(tx, langs)
+
}
+12 -1
appview/repo/index.go
···
})
}
+
tx, err := rp.db.Begin()
+
if err != nil {
+
return nil, err
+
}
+
defer tx.Rollback()
+
// update appview's cache
-
err = db.InsertRepoLanguages(rp.db, langs)
+
err = db.UpdateRepoLanguages(tx, f.RepoAt(), currentRef, langs)
if err != nil {
// non-fatal
log.Println("failed to cache lang results", err)
}
+
+
err = tx.Commit()
+
if err != nil {
+
return nil, err
+
}
}
var total int64
+14 -1
appview/state/knotstream.go
···
})
}
-
return db.InsertRepoLanguages(d, langs)
+
tx, err := d.Begin()
+
if err != nil {
+
return err
+
}
+
defer tx.Rollback()
+
+
// update appview's cache
+
err = db.UpdateRepoLanguages(tx, repo.RepoAt(), ref.Short(), langs)
+
if err != nil {
+
fmt.Printf("failed; %s\n", err)
+
// non-fatal
+
}
+
+
return tx.Commit()
}
func ingestPipeline(d *db.DB, source ec.Source, msg ec.Message) error {