forked from vcavallo/nostr-hypermedia
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathactions.go
More file actions
370 lines (328 loc) · 11.6 KB
/
actions.go
File metadata and controls
370 lines (328 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
package main
import (
"os"
"strings"
"nostr-server/internal/config"
)
// disabledActions holds globally disabled actions (parsed from ACTIONS_DISABLE env var)
var disabledActions map[string]bool
func init() {
disabledActions = make(map[string]bool)
if disabled := os.Getenv("ACTIONS_DISABLE"); disabled != "" {
for _, action := range strings.Split(disabled, ",") {
action = strings.TrimSpace(strings.ToLower(action))
if action != "" {
disabledActions[action] = true
}
}
}
}
// IsActionDisabled returns true if an action is globally disabled via ACTIONS_DISABLE
func IsActionDisabled(actionName string) bool {
return disabledActions[actionName]
}
// ActionDefinition defines a possible action on an event.
// This is the canonical source of truth for both HTML and Siren rendering.
type ActionDefinition struct {
Name string // Identifier: "reply", "react", "repost", "quote", "bookmark"
Title string // Display text
Method string // "GET" or "POST"
Href string // URL pattern (populated per-event)
Class string // CSS class for styling
Rel string // Link relation for semantic meaning (e.g., "reply", "bookmark", "author")
Icon string // Optional icon
IconOnly string // "always", "mobile", "desktop", or "" (never) - controls icon-only display
Fields []FieldDefinition // Form fields (for POST actions)
Completed bool // If true, action already performed (filled pill style, no-op on click)
Count int // Count to display (if HasCount is true in config)
HasCount bool // Whether to show count
GroupWith string // If set, this action appears in another action's dropdown
Amounts []int // Preset amounts for zap action dropdown (in sats)
HTarget string // Custom h-target selector (overrides default #page-content for GET)
HSwap string // Custom h-swap mode (overrides default "inner" for GET)
}
// FieldDefinition defines a form field for POST actions
type FieldDefinition struct {
Name string // Field name
Type string // "hidden", "text", "textarea"
Value string // Field value
}
// ActionContext provides context for determining which actions apply
type ActionContext struct {
EventID string
EventPubkey string
Kind int
DTag string // d-tag for addressable events (kind 30xxx)
RelayHint string // Relay hint for NIP-10 e-tags (from RelaysSeen)
ThreadRootID string // Thread root event ID (for inline replies)
IsBookmarked bool
IsReacted bool // Whether user has already reacted to this event
IsReposted bool // Whether user has already reposted this event
IsZapped bool // Whether user has already zapped this event
IsMuted bool // Whether the event's author is in user's mute list
ReplyCount int // Number of replies
RepostCount int // Number of reposts
ReactionCount int // Total reactions (consolidated, not by emoji)
ZapTotal int64 // Total zap amount in sats
LoggedIn bool
HasWallet bool // Whether user has a wallet connected
IsAuthor bool
CSRFToken string
ReturnURL string
LoginURL string // URL to redirect to for login
PageType string // "timeline", "thread", "profile" - affects action behavior
}
// StandardActions returns the display order for actions (from config)
func StandardActions() []string {
return ConfigGetDisplayOrder()
}
// ActionAppliesTo determines if an action applies to a given event kind
// Returns true if the action should be shown for this kind
func ActionAppliesTo(actionName string, kind int) bool {
return ConfigActionAppliesTo(actionName, kind)
}
// buildAction creates an ActionDefinition for a specific action and context
func buildAction(actionName string, ctx ActionContext) ActionDefinition {
return ConfigBuildAction(actionName, ctx)
}
// buildLoggedOutAction creates a disabled action that links to login
func buildLoggedOutAction(actionName string, ctx ActionContext) ActionDefinition {
return ConfigBuildLoggedOutAction(actionName, ctx)
}
// GetActionsForEvent returns the list of actions available for an event
// based on its kind, login state, and other context
func GetActionsForEvent(ctx ActionContext) []ActionDefinition {
var actions []ActionDefinition
// Check for kind-specific overrides (e.g., articles only show read + bookmark)
if overrideActions, hasOverride := ConfigGetKindOverride(ctx.Kind); hasOverride {
for _, actionName := range overrideActions {
if IsActionDisabled(actionName) {
continue
}
// For "read" action, always show it
// For other actions, require login
if actionName == "read" {
actions = append(actions, buildAction(actionName, ctx))
} else if ctx.LoggedIn {
actions = append(actions, buildAction(actionName, ctx))
}
}
// Also add registered actions for this kind
actions = append(actions, getRegisteredActionsForKind(ctx)...)
return actions
}
// For other kinds, iterate through standard actions
for _, actionName := range StandardActions() {
// Skip globally disabled actions
if IsActionDisabled(actionName) {
continue
}
// Skip actions with groupWith - they're added via ConfigGetGroupedActions
if ConfigActionHasGroupWith(actionName) {
continue
}
if !ActionAppliesTo(actionName, ctx.Kind) {
continue
}
if ctx.LoggedIn {
actions = append(actions, buildAction(actionName, ctx))
} else {
// Show logged-out version (links to login)
actions = append(actions, buildLoggedOutAction(actionName, ctx))
}
}
// Add grouped actions (those with groupWith set, not in displayOrder)
for _, actionName := range ConfigGetGroupedActions(ctx.Kind) {
if IsActionDisabled(actionName) {
continue
}
if ctx.LoggedIn {
actions = append(actions, buildAction(actionName, ctx))
} else {
actions = append(actions, buildLoggedOutAction(actionName, ctx))
}
}
// Add programmatically registered actions for this kind
actions = append(actions, getRegisteredActionsForKind(ctx)...)
return actions
}
// getRegisteredActionsForKind returns actions registered via RegisterKindAction
func getRegisteredActionsForKind(ctx ActionContext) []ActionDefinition {
var actions []ActionDefinition
// Get actions registered for this specific kind
kindActionNames := GetKindActions(ctx.Kind)
for _, actionName := range kindActionNames {
if IsActionDisabled(actionName) {
continue
}
// Check if this action is already in config (avoid duplicates)
if ConfigActionAppliesTo(actionName, ctx.Kind) {
continue
}
// Get the registered action
regAction, exists := GetRegisteredAction(actionName)
if !exists {
continue
}
// Check login requirement
if regAction.RequiresLogin && !ctx.LoggedIn {
// Build logged-out version
actions = append(actions, buildLoggedOutRegisteredAction(regAction, ctx))
} else {
actions = append(actions, BuildRegisteredAction(regAction, ctx))
}
}
return actions
}
// buildLoggedOutRegisteredAction creates a disabled action linking to login
func buildLoggedOutRegisteredAction(action *RegisteredAction, ctx ActionContext) ActionDefinition {
return ActionDefinition{
Name: action.Name,
Title: config.I18n(action.Config.TitleKey),
Method: "GET",
Href: ctx.LoginURL,
Class: action.Config.Class + " action-disabled",
}
}
// ToSirenAction converts an ActionDefinition to a SirenAction
func (a ActionDefinition) ToSirenAction() SirenAction {
var fields []SirenField
for _, f := range a.Fields {
fields = append(fields, SirenField{
Name: f.Name,
Type: f.Type,
Value: f.Value,
})
}
return SirenAction{
Name: a.Name,
Title: a.Title,
Method: a.Method,
Href: a.Href,
Type: "application/x-www-form-urlencoded",
Fields: fields,
}
}
// ToHTMLAction converts an ActionDefinition to an HTMLAction
func (a ActionDefinition) ToHTMLAction() HTMLAction {
var csrfToken string
var fields []HTMLField
for _, f := range a.Fields {
if f.Name == "csrf_token" {
csrfToken = f.Value
continue // Don't include in fields - rendered explicitly in template
}
fields = append(fields, HTMLField{
Name: f.Name,
Value: f.Value,
})
}
return HTMLAction{
Name: a.Name,
Title: a.Title,
Href: a.Href,
Method: a.Method,
Class: a.Class,
Rel: a.Rel,
Icon: a.Icon,
IconOnly: a.IconOnly,
CSRFToken: csrfToken,
Fields: fields,
Completed: a.Completed,
Count: a.Count,
HasCount: a.HasCount,
GroupWith: a.GroupWith,
Amounts: a.Amounts,
HTarget: a.HTarget,
HSwap: a.HSwap,
}
}
// HTMLActionGroup represents a primary action with optional grouped children
type HTMLActionGroup struct {
Primary HTMLAction // The primary action
Children []HTMLAction // Grouped actions that appear in dropdown
HasGroup bool // Whether this action has grouped children
}
// GroupActionsForKind organizes actions into groups based on GroupWith config.
// Actions with GroupWith are placed under their parent action's dropdown.
// Returns a list of action groups (primary actions with optional children).
func GroupActionsForKind(actions []ActionDefinition, kind int) []HTMLActionGroup {
// First pass: collect all actions and identify groupings
actionMap := make(map[string]HTMLAction)
groupChildren := make(map[string][]HTMLAction) // parent name -> children
for _, action := range actions {
htmlAction := action.ToHTMLAction()
if htmlAction.Name == "" {
continue
}
if htmlAction.GroupWith != "" {
// This action is grouped under another
groupChildren[htmlAction.GroupWith] = append(groupChildren[htmlAction.GroupWith], htmlAction)
} else {
// This is a primary action
actionMap[htmlAction.Name] = htmlAction
}
}
// Sort grouped children according to displayOrder
displayOrder := ConfigGetDisplayOrder()
displayOrderIndex := make(map[string]int)
for i, name := range displayOrder {
displayOrderIndex[name] = i
}
for parent, children := range groupChildren {
sortedChildren := make([]HTMLAction, len(children))
copy(sortedChildren, children)
// Sort by displayOrder position (actions not in displayOrder go last)
for i := 0; i < len(sortedChildren)-1; i++ {
for j := i + 1; j < len(sortedChildren); j++ {
iIdx, iExists := displayOrderIndex[sortedChildren[i].Name]
jIdx, jExists := displayOrderIndex[sortedChildren[j].Name]
// Actions not in displayOrder get max int value (go last)
if !iExists {
iIdx = 999999
}
if !jExists {
jIdx = 999999
}
if iIdx > jIdx {
sortedChildren[i], sortedChildren[j] = sortedChildren[j], sortedChildren[i]
}
}
}
groupChildren[parent] = sortedChildren
}
// Second pass: build groups in display order
var groups []HTMLActionGroup
processedActions := make(map[string]bool)
for _, name := range displayOrder {
action, exists := actionMap[name]
if !exists {
continue
}
group := HTMLActionGroup{
Primary: action,
Children: groupChildren[name],
HasGroup: len(groupChildren[name]) > 0,
}
groups = append(groups, group)
processedActions[name] = true
}
// Third pass: add any remaining actions not in displayOrder (e.g., kindOverride-specific actions)
// These are added at the beginning since they're typically kind-specific primary actions like "read"
var extraGroups []HTMLActionGroup
for name, action := range actionMap {
if processedActions[name] {
continue
}
group := HTMLActionGroup{
Primary: action,
Children: groupChildren[name],
HasGroup: len(groupChildren[name]) > 0,
}
extraGroups = append(extraGroups, group)
}
if len(extraGroups) > 0 {
groups = append(extraGroups, groups...)
}
return groups
}