1// Package dialog provides UI dialog implementations for the application layer.
2package dialog
3
4import (
5 "context"
6 "fmt"
7 "sync"
8
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/domain/entity"
11 "github.com/bnema/dumber/internal/logging"
12 "github.com/bnema/dumber/internal/ui/component"
13)
14
15type permissionPopup interface {
16 Show(ctx context.Context, heading, body string, callback func(allowed, persistent bool))
17}
18
19type permissionDialogRequest struct {
20 ctx context.Context
21 origin string
22 permTypes []entity.PermissionType
23 callback func(result port.PermissionDialogResult)
24}
25
26// PermissionDialog implements the port.PermissionDialogPresenter interface.
27// It uses a custom PermissionPopup overlay to sidestep the purego ConnectResponse bug
28// and match the app's custom UI style.
29type PermissionDialog struct {
30 popup permissionPopup
31
32 mu sync.Mutex
33 active bool
34 queue []permissionDialogRequest
35}
36
37// NewPermissionDialog creates a new permission dialog presenter.
38// The popup is created once and reused for each permission request.
39func NewPermissionDialog(popup *component.PermissionPopup) *PermissionDialog {
40 return &PermissionDialog{
41 popup: popup,
42 }
43}
44
45// ShowPermissionDialog displays a permission request dialog to the user.
46func (d *PermissionDialog) ShowPermissionDialog(
47 ctx context.Context,
48 origin string,
49 permTypes []entity.PermissionType,
50 callback func(result port.PermissionDialogResult),
51) {
52 req := permissionDialogRequest{
53 ctx: ctx,
54 origin: origin,
55 permTypes: permTypes,
56 callback: callback,
57 }
58
59 if !d.enqueueOrStart(req) {
60 return
61 }
62
63 d.showRequest(req)
64}
65
66func (d *PermissionDialog) enqueueOrStart(req permissionDialogRequest) bool {
67 d.mu.Lock()
68 defer d.mu.Unlock()
69
70 if d.active {
71 d.queue = append(d.queue, req)
72 return false
73 }
74
75 d.active = true
76 return true
77}
78
79func (d *PermissionDialog) showRequest(req permissionDialogRequest) {
80 ctx := req.ctx
81 origin := req.origin
82 permTypes := req.permTypes
83 callback := req.callback
84
85 log := logging.FromContext(ctx)
86
87 if d.popup == nil {
88 log.Error().Msg("permission popup not available")
89 callback(port.PermissionDialogResult{Allowed: false, Persistent: false})
90 d.showNextQueuedRequest()
91 return
92 }
93
94 // Build dialog text
95 heading := d.buildHeading(permTypes)
96 body := d.buildBody(origin, permTypes)
97
98 d.popup.Show(ctx, heading, body, func(allowed, persistent bool) {
99 log.Debug().
100 Bool("allowed", allowed).
101 Bool("persistent", persistent).
102 Msg("permission popup response")
103 callback(port.PermissionDialogResult{Allowed: allowed, Persistent: persistent})
104 d.showNextQueuedRequest()
105 })
106
107 log.Debug().
108 Str("origin", origin).
109 Strs("types", entity.PermissionTypesToStrings(permTypes)).
110 Msg("showing permission popup")
111}
112
113func (d *PermissionDialog) showNextQueuedRequest() {
114 d.mu.Lock()
115 if len(d.queue) == 0 {
116 d.active = false
117 d.mu.Unlock()
118 return
119 }
120
121 next := d.queue[0]
122 d.queue = d.queue[1:]
123 d.mu.Unlock()
124
125 d.showRequest(next)
126}
127
128// buildHeading creates the dialog heading based on permission types.
129func (d *PermissionDialog) buildHeading(permTypes []entity.PermissionType) string {
130 hasMic := false
131 hasCam := false
132 hasDisplay := false
133
134 for _, pt := range permTypes {
135 switch pt {
136 case entity.PermissionTypeMicrophone:
137 hasMic = true
138 case entity.PermissionTypeCamera:
139 hasCam = true
140 case entity.PermissionTypeDisplay:
141 hasDisplay = true
142 }
143 }
144
145 switch {
146 case hasMic && hasCam && hasDisplay:
147 return "Allow Microphone, Camera, and Screen Sharing?"
148 case hasMic && hasDisplay:
149 return "Allow Microphone and Screen Sharing?"
150 case hasCam && hasDisplay:
151 return "Allow Camera and Screen Sharing?"
152 case hasMic && hasCam:
153 return "Allow Microphone and Camera?"
154 case hasMic:
155 return "Allow Microphone Access?"
156 case hasCam:
157 return "Allow Camera Access?"
158 case hasDisplay:
159 return "Allow Screen Sharing?"
160 default:
161 return "Allow Permission?"
162 }
163}
164
165// buildBody creates the dialog body text.
166func (d *PermissionDialog) buildBody(origin string, permTypes []entity.PermissionType) string {
167 hasMic := false
168 hasCam := false
169 hasDisplay := false
170
171 for _, pt := range permTypes {
172 switch pt {
173 case entity.PermissionTypeMicrophone:
174 hasMic = true
175 case entity.PermissionTypeCamera:
176 hasCam = true
177 case entity.PermissionTypeDisplay:
178 hasDisplay = true
179 }
180 }
181
182 var action string
183 switch {
184 case hasMic && hasCam && hasDisplay:
185 action = "access your microphone and camera, and share your screen"
186 case hasMic && hasDisplay:
187 action = "access your microphone and share your screen"
188 case hasCam && hasDisplay:
189 action = "access your camera and share your screen"
190 case hasMic && hasCam:
191 action = "access your microphone and camera"
192 case hasMic:
193 action = "access your microphone"
194 case hasCam:
195 action = "access your camera"
196 case hasDisplay:
197 action = "share your screen"
198 default:
199 action = "access your device"
200 }
201
202 return fmt.Sprintf("%s wants to %s.", origin, action)
203}
204
205// Ensure PermissionDialog implements the interface.
206var _ port.PermissionDialogPresenter = (*PermissionDialog)(nil)