tinyrelayfile
tinyrelay/tinyclient/mobile_header.go/

tinyclient/mobile_header.go raw
1package tinyclient
2
3import (
4 "net/url"
5 "strings"
6)
7
8// 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 string
12 Subtitle string
13 BackURL string
14 BackLabel string
15 PubKey string
16 RoomID string
17 Picture string
18}
19
20// mobileHeader derives a compact header from rendered page data. Private and
21// failed room pages deliberately use generic values so hidden room metadata
22// 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 = next
38 result.BackLabel = "Back"
39 }
40 return result
41 }
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.View
56 }
57 return result
58 }
59 return generalMobileHeader(data)
60}
61
62func 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 = name
67 }
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.event
78 }
79 if eventIDPattern.MatchString(root) {
80 result.BackURL = "/rooms/" + route.id + "#msg-" + root
81 }
82 result.BackLabel = "Back to room"
83 return result
84 }
85 result.BackURL, result.BackLabel = "/chat", "Back to chats"
86 return result
87}
88
89func generalMobileHeader(data PageData) mobileHeaderData {
90 path := data.Path
91 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 = repo
107 }
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.Slug
156 }
157 return result
158}
159
160func mobileReturnPath(raw, base string) (string, bool) {
161 if raw == "" || strings.IndexFunc(raw, func(r rune) bool { return r < 0x20 || r == '\\' }) >= 0 {
162 return "", false
163 }
164 u, err := url.Parse(raw)
165 if err != nil || u.IsAbs() || u.Host != "" || u.Path == "/signin" || !strings.HasPrefix(u.Path, "/") {
166 return "", false
167 }
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 "", false
171 }
172 if base != "" && strings.HasPrefix(u.Path, "/r/") && !strings.HasPrefix(u.Path, strings.TrimSuffix(base, "/")+"/") {
173 return "", false
174 }
175 return raw, true
176}
177
178func eventShortTitle(value any, fallback string) string {
179 row := valueMap(value)
180 if post := valueMap(row["post"]); post != nil {
181 row = post
182 }
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 fallback
192}
193
194func shortHeaderText(value string) string {
195 value = strings.TrimSpace(value)
196 if len([]rune(value)) > 64 {
197 return string([]rune(value)[:61]) + "..."
198 }
199 return value
200}
201
202func 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 title
206 }
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 title
218 }
219 return "Tiny"
220}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs