clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / tinyclient / mobile_header.go
mobile_header.go 7.1K
1package tinyclient23import (4 "net/url"5 "strings"6)78// mobileHeaderData is the small, route-aware context used by the mobile shell.9// It contains only data that has already passed page access checks.10type mobileHeaderData struct {11 Title string12 Subtitle string13 BackURL string14 BackLabel string15 PubKey string16 RoomID string17 Picture string18}1920// mobileHeader derives a compact header from rendered page data. Private and21// failed room pages deliberately use generic values so hidden room metadata22// cannot appear in the shell.23func mobileHeader(data PageData) mobileHeaderData {24 if data.Private {25 return mobileHeaderData{Title: "Sign in", Subtitle: data.Slug}26 }27 if data.Tab == "signin" {28 title := "Sign in"29 if data.Actor != "" {30 title = "Account"31 if data.Query.Get("connect") != "" {32 title = "Connect signer"33 }34 }35 result := mobileHeaderData{Title: title, Subtitle: data.Slug, BackLabel: "Back"}36 if next, ok := mobileReturnPath(data.Query.Get("next"), data.Base); ok {37 result.BackURL = next38 result.BackLabel = "Back"39 }40 return result41 }42 if data.Error != "" && roomRoute(data.Path).tab != "" {43 return mobileHeaderData{Title: "Chat", BackURL: "/chat", BackLabel: "Back to chats"}44 }45 route := roomRoute(data.Path)46 switch route.tab {47 case "room":48 return roomHeader(data, route, false)49 case "thread":50 return roomHeader(data, route, true)51 }52 if data.Tab == "direct" || strings.HasPrefix(data.Path, "/chat/dm/") {53 result := mobileHeaderData{Title: "Direct chat", Subtitle: "Private conversation", BackURL: "/chat", BackLabel: "Back to chats"}54 if eventIDPattern.MatchString(data.View) {55 result.PubKey = data.View56 }57 return result58 }59 return generalMobileHeader(data)60}6162func roomHeader(data PageData, route roomPath, thread bool) mobileHeaderData {63 result := mobileHeaderData{Title: "Group chat", Subtitle: "Chat", RoomID: route.id}64 room := valueMap(valueMap(data.Event)["room"])65 if name := plainString(room["name"]); name != "" {66 result.Title = name67 }68 result.Picture = socialMediaURL(plainString(room["picture"]))69 if thread {70 result.Title = "Thread"71 result.Subtitle = plainString(room["name"])72 if result.Subtitle == "" {73 result.Subtitle = "Group chat"74 }75 root := plainString(valueMap(valueMap(data.Event)["root"])["id"])76 if !eventIDPattern.MatchString(root) {77 root = route.event78 }79 if eventIDPattern.MatchString(root) {80 result.BackURL = "/rooms/" + route.id + "#msg-" + root81 }82 result.BackLabel = "Back to room"83 return result84 }85 result.BackURL, result.BackLabel = "/chat", "Back to chats"86 return result87}8889func generalMobileHeader(data PageData) mobileHeaderData {90 path := data.Path91 result := mobileHeaderData{Title: mobileTitle(path, data), Subtitle: data.Slug, BackLabel: "Back"}92 if data.Tab == "social-thread" {93 result.Title = eventShortTitle(data.Event, "Social")94 result.Subtitle = "Social"95 }96 if data.Tab == "file" {97 result.Title = eventShortTitle(data.Event, "File")98 result.Subtitle = "Files"99 }100 if data.Tab == "files" && data.Query.Get("view") == "sites" {101 result.Title = "Sites"102 result.Subtitle = "Files"103 }104 if data.Tab == "repo" {105 if repo := data.Query.Get("repo"); repo != "" {106 result.Title = repo107 }108 result.Subtitle = repoView(data.Query)109 }110 if data.Tab == "wikipage" {111 result.Title = eventShortTitle(data.Event, data.Query.Get("d"))112 result.Subtitle = "Wiki"113 }114 switch {115 case path == "/" || path == "":116 case path == "/chat" || path == "/social" || path == "/files" || path == "/wiki" || path == "/repos":117 result.BackURL = "/"118 case path == "/manage" || path == "/manage/people":119 result.BackURL = "/"120 result.BackLabel = "Back to home"121 case strings.HasPrefix(path, "/manage/"):122 result.BackURL = "/manage/people"123 result.BackLabel = "Back to manage"124 case path == "/sites":125 result.BackURL = "/"126 case path == "/file":127 result.BackURL = "/files"128 result.BackLabel = "Back to files"129 case path == "/repo":130 result.BackURL = "/repos"131 result.BackLabel = "Back to repositories"132 case path == "/profile":133 result.BackURL = "/account"134 result.BackLabel = "Back to account"135 case strings.HasPrefix(path, "/social/"):136 result.BackURL = "/social"137 result.BackLabel = "Back to social"138 case strings.HasPrefix(path, "/files/"):139 result.BackURL = "/files"140 result.BackLabel = "Back to files"141 case strings.HasPrefix(path, "/wiki/"):142 result.BackURL = "/wiki"143 result.BackLabel = "Back to wiki"144 case strings.HasPrefix(path, "/repos/"):145 result.BackURL = "/repos"146 result.BackLabel = "Back to repositories"147 case path != "/social" && path != "/files" && path != "/wiki" && path != "/repos":148 result.BackURL = "/"149 }150 if strings.HasPrefix(path, "/manage/") {151 result.Subtitle = "Manage"152 }153 if data.Tab == "signin" {154 result.Title = "Sign in"155 result.Subtitle = data.Slug156 }157 return result158}159160func mobileReturnPath(raw, base string) (string, bool) {161 if raw == "" || strings.IndexFunc(raw, func(r rune) bool { return r < 0x20 || r == '\\' }) >= 0 {162 return "", false163 }164 u, err := url.Parse(raw)165 if err != nil || u.IsAbs() || u.Host != "" || u.Path == "/signin" || !strings.HasPrefix(u.Path, "/") {166 return "", false167 }168 decodedPath, err := url.PathUnescape(u.Path)169 if err != nil || strings.IndexFunc(decodedPath, func(r rune) bool { return r < 0x20 || r == '\\' }) >= 0 {170 return "", false171 }172 if base != "" && strings.HasPrefix(u.Path, "/r/") && !strings.HasPrefix(u.Path, strings.TrimSuffix(base, "/")+"/") {173 return "", false174 }175 return raw, true176}177178func eventShortTitle(value any, fallback string) string {179 row := valueMap(value)180 if post := valueMap(row["post"]); post != nil {181 row = post182 }183 for _, key := range []string{"name", "filename", "title", "d"} {184 if title := strings.TrimSpace(plainString(row[key])); title != "" && !eventIDPattern.MatchString(title) {185 return shortHeaderText(title)186 }187 }188 if content := strings.TrimSpace(plainString(row["content"])); content != "" {189 return shortHeaderText(strings.Split(content, "\n")[0])190 }191 return fallback192}193194func shortHeaderText(value string) string {195 value = strings.TrimSpace(value)196 if len([]rune(value)) > 64 {197 return string([]rune(value)[:61]) + "..."198 }199 return value200}201202func mobileTitle(path string, data PageData) string {203 known := map[string]string{"/": "Home", "/home": "Home", "/search": "Search", "/social": "Social", "/chat": "Chat", "/files": "Files", "/file": "File", "/wiki": "Wiki", "/repos": "Repositories", "/repo": "Repository", "/approvals": "Approvals", "/profile": "Profile", "/account": "Account", "/manage": "Manage", "/manage/people": "People", "/manage/agents": "Agents", "/manage/moderation": "Moderation", "/manage/rules": "Rules", "/manage/identity": "Identity", "/manage/connect": "Connect", "/manage/owner": "Owner", "/manage/sync": "Sync", "/manage/data": "Data", "/manage/views": "Views", "/manage/health": "Health", "/sites": "Sites"}204 if title, ok := known[path]; ok {205 return title206 }207 if strings.HasPrefix(path, "/social/") {208 return "Social"209 }210 if strings.HasPrefix(path, "/wiki/") {211 return "Wiki"212 }213 if strings.HasPrefix(path, "/repos/") {214 return "Repositories"215 }216 if title := strings.TrimSuffix(data.Title, " | "+data.Slug); title != "" && !eventIDPattern.MatchString(title) {217 return title218 }219 return "Tiny"220}