Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

appview/timeline: show notifications on timeline

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
date (Jun 8, 2026, 2:45 PM +0100) commit 8247cc28 parent a04c535b change-id ymozrmlp
+143 -29
+15
appview/models/repo.go
··· 7 7 8 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 9 securejoin "github.com/cyphar/filepath-securejoin" 10 + enry "github.com/go-enry/go-enry/v2" 10 11 "tangled.org/core/api/tangled" 11 12 ) 12 13 ··· 112 113 IssueCount IssueCount 113 114 PullCount PullCount 114 115 ForkCount int 116 + } 117 + 118 + // returns the first file extension for the language ("ts" for typescript) as 119 + // an uppercase string 120 + func (s *RepoStats) LangShortName() string { 121 + if s == nil || s.Language == "" { 122 + return "" 123 + } 124 + exts := enry.GetLanguageExtensions(s.Language) 125 + if len(exts) > 0 { 126 + // extensions include the leading dot, e.g. ".ts" -> "TS" 127 + return strings.ToUpper(strings.TrimPrefix(exts[0], ".")) 128 + } 129 + return s.Language 115 130 } 116 131 117 132 type IssueCount struct {
+1
appview/pages/pages.go
··· 407 407 GfiLabel *models.LabelDefinition 408 408 BlueskyPosts []models.BskyPost 409 409 VouchSuggestions []models.VouchSuggestion 410 + Notifications []*models.NotificationWithEntity 410 411 // ShowNewsletter controls whether the newsletter widget/CTA is rendered. 411 412 // For logged-in users it reflects their newsletter_preferences row; for 412 413 // anonymous visitors it is always true (dismissal falls back to
+4 -4
appview/pages/templates/timeline/fragments/announcements.html
··· 1 1 {{ define "timeline/fragments/announcements" }} 2 - <div> 3 - <h3 class="text-xl font-bold dark:text-white hidden md:flex items-center gap-4 px-4 pb-4"> 4 - {{ i "megaphone" "size-5 flex-shrink-0" }} 2 + <div class="border border border-gray-200 dark:border-gray-700 rounded-sm divide-y divide-gray-200 dark:divide-gray-700"> 3 + <h3 class="text-base dark:text-white hidden md:flex items-center gap-2 px-4 py-2 bg-white/50 dark:bg-gray-800/50"> 4 + {{ i "megaphone" "size-4 flex-shrink-0" }} 5 5 Announcements 6 6 </h3> 7 - <div class="flex flex-col gap-3"> 7 + <div class="flex flex-col gap-3 p-2 bg-white dark:bg-gray-800"> 8 8 {{ template "vouchAnnouncement" . }} 9 9 </div> 10 10 </div>
+1 -1
appview/pages/templates/timeline/fragments/newsletterForm.html
··· 35 35 placeholder="your@email.com" 36 36 required 37 37 class="flex-1 min-w-0 text-sm py-2" /> 38 - <button type="submit" class="btn whitespace-nowrap px-4 pb-0 gap-2"> 38 + <button type="submit" class="btn whitespace-nowrap px-4 gap-2"> 39 39 {{ i "mail-plus" "size-4 inline" }} 40 40 Subscribe 41 41 </button>
+35
appview/pages/templates/timeline/fragments/notifications.html
··· 1 + {{ define "timeline/fragments/notifications" }} 2 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm divide-y divide-gray-200 dark:divide-gray-700"> 3 + <a href="/notifications" class="flex items-center justify-between bg-white/50 dark:bg-gray-800/50 no-underline hover:no-underline"> 4 + <h3 class="text-base dark:text-white flex items-center gap-2 px-4 py-2"> 5 + {{ i "bell" "size-4 flex-shrink-0" }} 6 + Notifications 7 + </h3> 8 + <span class="text-sm font-normal flex items-center gap-1 pr-4"> 9 + View all {{ i "arrow-right" "size-3.5" }} 10 + </span> 11 + </a> 12 + 13 + {{ if not .LoggedInUser }} 14 + <div class="px-4 py-8 text-sm text-center text-gray-400 dark:text-gray-500"> 15 + <div class="w-10 h-10 mx-auto mb-3 text-gray-300 dark:text-gray-600"> 16 + {{ i "bell-off" "w-10 h-10" }} 17 + </div> 18 + <a href="/login">Log in</a> to view your inbox 19 + </div> 20 + {{ else if not .Notifications }} 21 + <div class="px-4 py-8 text-sm text-center text-gray-400 dark:text-gray-500"> 22 + <div class="w-10 h-10 mx-auto mb-3 text-gray-300 dark:text-gray-600"> 23 + {{ i "inbox" "w-10 h-10" }} 24 + </div> 25 + <p>All caught up!</p> 26 + </div> 27 + {{ else }} 28 + <div class="divide-y divide-gray-200 dark:divide-gray-700"> 29 + {{ range .Notifications }} 30 + {{ template "notifications/fragments/item" . }} 31 + {{ end }} 32 + </div> 33 + {{ end }} 34 + </div> 35 + {{ end }}
+2 -2
appview/pages/templates/timeline/fragments/timeline.html
··· 1 1 {{ define "timeline/fragments/timeline" }} 2 2 <div class="py-4"> 3 - <h3 class="text-xl font-bold dark:text-white flex items-center gap-4 px-4 pb-4"> 4 - {{ i "gallery-vertical" "size-5 flex-shrink-0" }} 3 + <h3 class="text-base dark:text-white flex items-center gap-2 px-4 py-2"> 4 + {{ i "gallery-vertical" "size-4 flex-shrink-0" }} 5 5 Timeline 6 6 </h3> 7 7
+50 -9
appview/pages/templates/timeline/fragments/trending.html
··· 1 1 {{ define "timeline/fragments/trending" }} 2 - <div> 3 - <h3 class="text-xl font-bold dark:text-white flex items-center gap-4 px-4 pb-4"> 4 - {{ i "trending-up" "size-5 flex-shrink-0" }} 2 + <div class="border border border-gray-200 dark:border-gray-700 rounded-sm divide-y divide-gray-200 dark:divide-gray-700"> 3 + <h3 class="text-base dark:text-white flex items-center gap-2 px-4 py-2 bg-white/50 dark:bg-gray-800/50"> 4 + {{ i "trending-up" "size-4 flex-shrink-0" }} 5 5 Trending 6 6 </h3> 7 7 {{ if .Repos }} 8 - <div class="flex gap-4 overflow-x-auto scrollbar-hide items-stretch md:flex-col md:overflow-x-visible"> 8 + <table class="w-full py-1 bg-white dark:bg-gray-800"> 9 + <tbody> 9 10 {{ range $index, $repo := .Repos }} 10 - <div class="flex-none w-96 border border-gray-200 dark:border-gray-700 rounded-sm md:flex-none md:w-auto"> 11 - {{ template "user/fragments/repoCard" (list $ $repo true) }} 12 - </div> 11 + {{ template "singleLineRepo" (list $ $repo) }} 13 12 {{ end }} 14 - </div> 13 + </tbody> 14 + </table> 15 15 {{ else }} 16 - <div class="p-6 border border-gray-200 dark:border-gray-700 rounded-sm text-sm text-gray-500 dark:text-gray-400 text-center"> 16 + <div class="p-6 text-sm text-gray-500 dark:text-gray-400 text-center"> 17 17 No trending repositories this week 18 18 </div> 19 19 {{ end }} 20 20 </div> 21 21 {{ end }} 22 + 23 + {{ define "singleLineRepo" }} 24 + {{ $root := index . 0 }} 25 + {{ $repo := index . 1 }} 26 + {{ with $repo }} 27 + {{ $repoOwner := resolve .Did }} 28 + <tr class="relative hover:bg-gray-100/50 dark:hover:bg-gray-700/50 group"> 29 + <td class="pl-4 pr-2 py-3 text-right text-gray-400 tabular-nums whitespace-nowrap align-top"> 30 + <div class="flex text-sm items-center justify-end gap-1"> 31 + {{ i "star" "w-3 h-3 fill-current shrink-0" }} 32 + {{ if .RepoStats }} 33 + <span>{{ scaleFmt .RepoStats.StarCount }}</span> 34 + {{ else }} 35 + <span>0</span> 36 + {{ end }} 37 + </div> 38 + </td> 39 + <td class="px-2 py-3 w-full max-w-0"> 40 + <a href="/{{ $repoOwner }}/{{ .Slug }}" class="absolute inset-0 no-underline" aria-label="{{ $repoOwner }}/{{ .Name }}"></a> 41 + <span class="text-sm font-medium dark:text-white truncate block">{{ $repoOwner }}/{{ .Name }}</span> 42 + {{ with .Description }} 43 + <span class="text-xs text-gray-500 dark:text-gray-400 truncate line-clamp-1 block">{{ . | description }}</span> 44 + {{ end }} 45 + </td> 46 + <td class="pl-2 pr-4 py-3 text-gray-400 text-sm whitespace-nowrap font-mono align-top"> 47 + {{ if and .RepoStats .RepoStats.Language }} 48 + {{ $lang := .RepoStats.Language }} 49 + {{ $display := $lang }} 50 + {{ if gt (len $lang) 6 }} 51 + {{ $display = .RepoStats.LangShortName }} 52 + {{ end }} 53 + <div class="flex items-center gap-1.5"> 54 + {{ template "repo/fragments/colorBall" (dict "color" (langColor $lang)) }} 55 + <span>{{ $display }}</span> 56 + </div> 57 + {{ end }} 58 + </td> 59 + </tr> 60 + {{ end }} 61 + {{ end }} 62 +
+6 -6
appview/pages/templates/timeline/fragments/vouchSuggestions.html
··· 1 1 {{ define "timeline/fragments/vouchSuggestions" }} 2 2 {{ if and .LoggedInUser .VouchSuggestions }} 3 - <div> 4 - <div class="flex items-center justify-between"> 5 - <h3 class="text-xl font-bold dark:text-white flex items-center gap-4 px-4 pb-4"> 6 - {{ i "shield-plus" "size-5 flex-shrink-0" }} 3 + <div class="border border border-gray-200 dark:border-gray-700 rounded-sm divide-y divide-gray-200 dark:divide-gray-700"> 4 + <a href="/{{ .LoggedInUser.Did }}?tab=vouches" class="flex items-center justify-between bg-white/50 dark:bg-gray-800/50 no-underline hover:no-underline"> 5 + <h3 class="text-base dark:text-white flex items-center gap-2 px-4 py-2"> 6 + {{ i "shield-plus" "size-4 flex-shrink-0" }} 7 7 Suggested 8 8 </h3> 9 - <a href="/{{ .LoggedInUser.Did }}?tab=vouches" class="text-sm font-normal hover:no-underline no-underline flex items-center gap-1 pb-4 pr-4"> 9 + <span class="text-sm font-normal flex items-center gap-1 pr-4"> 10 10 View all {{ i "arrow-right" "size-3.5" }} 11 11 </a> 12 12 </div> 13 - <div class="flex flex-col gap-4 bg-white dark:bg-gray-800 p-4 rounded-sm border border-gray-200 dark:border-gray-700 shadow-sm"> 13 + <div class="flex flex-col gap-4 bg-white dark:bg-gray-800 p-4"> 14 14 {{ range .VouchSuggestions }} 15 15 <div class="flex items-center gap-3 "> 16 16 {{ template "user/fragments/picLink" (list .Did.String "size-10") }}
+14 -7
appview/pages/templates/timeline/timeline.html
··· 8 8 {{ end }} 9 9 10 10 {{ define "mainLayout" }} 11 - <div class="flex-grow"> 12 - <div class="max-w-screen-lg mx-auto flex flex-col gap-4"> 13 - <main>{{ block "content" . }}{{ end }}</main> 14 - </div> 11 + <div class="flex-grow flex flex-col"> 12 + <main class="flex-grow flex flex-col">{{ block "content" . }}{{ end }}</main> 15 13 </div> 16 14 {{ end }} 17 15 18 16 {{ define "content" }} 19 - <div id="timeline-grid" class="flex flex-col gap-4 md:grid md:grid-cols-3"> 17 + <div id="timeline-grid" class="flex flex-col md:grid md:grid-cols-8"> 18 + 19 + <div class="hidden md:flex md:col-span-2 md:flex-col md:gap-6 md:pt-4 md:px-4"> 20 + {{ template "timeline/fragments/notifications" . }} 21 + </div> 20 22 21 - <div class="order-3 md:order-none md:col-span-2 md:row-start-1"> 23 + <div class="order-2 md:order-none md:col-start-3 md:row-start-1 md:col-span-4"> 22 24 {{ template "timeline/fragments/timeline" . }} 23 25 </div> 24 26 25 - <div class="order-1 md:order-none flex flex-col gap-6 md:col-start-3 md:row-start-1 md:pt-4"> 27 + <div class="order-1 md:order-none flex flex-col gap-4 md:col-start-7 md:row-start-1 md:col-span-2 md:pt-4 md:px-4"> 26 28 {{ if .ShowNewsletter }} 27 29 <div id="newsletter-col" class="order-first md:order-last"> 28 30 {{ template "timeline/fragments/newsletterWidget" . }} 29 31 </div> 30 32 {{ end }} 33 + 31 34 {{ template "timeline/fragments/announcements" . }} 35 + 36 + {{ if and .LoggedInUser .VouchSuggestions }} 32 37 <div class="hidden md:block"> 33 38 {{ template "timeline/fragments/vouchSuggestions" . }} 34 39 </div> 40 + {{ end }} 41 + 35 42 {{ template "timeline/fragments/trending" . }} 36 43 </div> 37 44 </div>
+15
appview/state/timeline.go
··· 8 8 "tangled.org/core/appview/models" 9 9 "tangled.org/core/appview/oauth" 10 10 "tangled.org/core/appview/pages" 11 + "tangled.org/core/appview/pagination" 11 12 "tangled.org/core/orm" 12 13 ) 13 14 ··· 72 73 // non-fatal 73 74 } 74 75 76 + var notifications []*models.NotificationWithEntity 77 + if user != nil { 78 + notifications, err = db.GetNotificationsWithEntities( 79 + s.db, 80 + pagination.Page{Limit: 5, Offset: 0}, 81 + orm.FilterEq("recipient_did", user.Did), 82 + orm.FilterEq("read", 0), 83 + ) 84 + if err != nil { 85 + s.logger.Error("failed to get notifications for timeline", "err", err) 86 + } 87 + } 88 + 75 89 var vouchSuggestions []models.VouchSuggestion 76 90 if user != nil { 77 91 vouchSuggestions, err = db.GetVouchSuggestions(s.db, user.Did, 3) ··· 100 114 Repos: repos, 101 115 GfiLabel: gfiLabel, 102 116 VouchSuggestions: vouchSuggestions, 117 + Notifications: notifications, 103 118 ShowNewsletter: s.showNewsletter(user), 104 119 }) 105 120 }