nandi/frqpublic Fork 0
9cc16c7
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Do not leave a ghost in the call, and use the window

Two things a real call found.

Dropping out was silent. freeq counts a participant until an av-leave says
otherwise, and when the media plane ended under us we cleared the local
call and told nobody — so the room still counted this device, and pressing
Join again added a second one beside it. That is how a channel came to
report seven people in a call with two. Any exit we did not ask for now
announces itself first, while the session id it needs is still there to
read. A failure announces too: the reason stays on screen for the person,
but the server is told either way, because we are out of the call whether
or not they have read why yet.

And the wall was too timid. A third of the height held one person to a
440-point tile on a 1920x1060 screen with fourteen hundred points of empty
width beside them, which reads as video that will not scale, because in
every way the eye can tell it does not. Half the height leaves the
conversation legible underneath while letting a maximised window be worth
maximising: three people go from 440 points to 620.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-08-30T18:00:23-07:00 Browse files
9cc16c7 parent: ddebf2a
modified src/frq/app.jolt +4 -0
@@ -807,5 +807,9 @@
807807 ;; else that touches a node belongs to the loop thread, and this is glimmer's
808808 ;; way of getting onto it.
809809 (av/init-logging!)
810+ ;; Who to tell when a call ends under us rather than at our asking. Set here
811+ ;; rather than in frq.av because sending a TAGMSG needs the connection, and
812+ ;; that belongs to the state layer.
813+ (reset! av/on-dropped s/announce-leave!)
810814 (vidya/after! 0 av/install-pump!)
811815 (ui/run app :title "frq" :width 520 :height 860))
@@ -807,5 +807,9 @@
807 ;; else that touches a node belongs to the loop thread, and this is glimmer's807 ;; else that touches a node belongs to the loop thread, and this is glimmer's
808 ;; way of getting onto it.808 ;; way of getting onto it.
809 (av/init-logging!)809 (av/init-logging!)
810+ ;; Who to tell when a call ends under us rather than at our asking. Set here
811+ ;; rather than in frq.av because sending a TAGMSG needs the connection, and
812+ ;; that belongs to the state layer.
813+ (reset! av/on-dropped s/announce-leave!)
810 (vidya/after! 0 av/install-pump!)814 (vidya/after! 0 av/install-pump!)
811 (ui/run app :title "frq" :width 520 :height 860))815 (ui/run app :title "frq" :width 520 :height 860))
modified src/frq/av.jolt +53 -15
@@ -178,6 +178,17 @@
178178 ;; The last thing the media plane failed with, for the line under the controls.
179179 (defonce media-error (atom nil))
180180
181+;; What to do when this device stops being in a call without having asked to.
182+;;
183+;; The media plane failing is not the server hearing about it: freeq counts a
184+;; participant until an `av-leave` says otherwise. Dropping out quietly leaves
185+;; a ghost in the room, and pressing Join again adds a second one — which is
186+;; how a channel ends up reporting seven people in a call with two.
187+;;
188+;; `frq.state` puts the TAGMSG here. This namespace cannot send one itself
189+;; without depending on the client that owns the connection.
190+(defonce on-dropped (atom nil))
191+
181192 ;; Feeds pushed to Vidya, so the ones that stop can be dropped again. Without
182193 ;; this the last frame of someone who left hangs on the wall for the rest of
183194 ;; the call.
@@ -249,9 +260,11 @@
249260 (reset! feeds []))
250261
251262 (defn stop-media!
252- "Leave the media plane and forget the call. Signaling is the caller's to send
253- — `leave!` does both, and this is what is left when the server ended it for
254- us."
263+ "Leave the media plane and forget the call, telling nobody.
264+
265+ For the two cases where the server already knows: it ended the call itself,
266+ or the caller is about to send an `av-leave` of its own. Anything else wants
267+ `dropped!`, or freeq goes on counting a participant who is not there."
255268 []
256269 (raw-stop)
257270 (drop-feeds!)
@@ -259,6 +272,18 @@
259272 (reset! local-call nil)
260273 (reset! media-error nil))
261274
275+(defn dropped!
276+ "We are out of the call and did not ask to be — the media plane failed, or
277+ the transport went away under it.
278+
279+ Tells whoever registered `on-dropped` first, while the session id and
280+ instance it needs are still here to be read, and only then forgets them."
281+ []
282+ (when-let [announce @on-dropped]
283+ (when-let [call @local-call]
284+ (try (announce call) (catch Exception _ nil))))
285+ (stop-media!))
286+
262287 (defn- start-media!
263288 "Dial the SFU for the call we have already joined over IRC.
264289
@@ -374,12 +399,21 @@
374399 :has-camera? (not (zero? (raw-status-has-camera)))
375400 :has-mic? (not (zero? (raw-status-has-mic))))))
376401
402+ ;; Both of these are the call ending underneath us rather than at
403+ ;; our request, so both have to be announced. A failure keeps the
404+ ;; local call up afterwards so the reason stays on screen — but the
405+ ;; server is told either way, because we are no longer in the call
406+ ;; whether or not the person has read why yet.
377407 (= code status-ended)
378- (stop-media!)
408+ (dropped!)
379409
380410 (= code status-failed)
381- (do (reset! media-error (status-text))
382- (swap! local-call #(when % (assoc % :media :failed)))))
411+ (let [why (status-text)
412+ call @local-call]
413+ (when-let [announce @on-dropped]
414+ (when call (try (announce call) (catch Exception _ nil))))
415+ (reset! media-error why)
416+ (swap! local-call #(when % (assoc % :media :failed)))))
383417 (recur)))))
384418
385419 (defn- pump-frames!
@@ -498,20 +532,24 @@
498532 ;; A tile narrower than this is not a face, it is a thumbnail of one.
499533 (def ^:private min-tile 96)
500534
501-;; And an upper bound, so that one person alone does not become a wall-sized
502-;; portrait in a maximised window. Generous rather than tight — what actually
503-;; stops tiles growing is the height budget below, and this is only here so
504-;; there is an answer on a screen tall enough not to bind.
505-(def ^:private max-tile 480)
535+;; And an upper bound, so one person alone does not become a wall-sized
536+;; portrait. Generous rather than tight — what actually stops tiles growing is
537+;; the height budget below, and this is only here so there is an answer on a
538+;; screen tall enough that it never binds.
539+(def ^:private max-tile 720)
506540
507541 ;; What a tile costs in height beyond its picture: the name under it, and the
508542 ;; gap to the row below.
509543 (def ^:private tile-label 22)
510544
511-;; The wall's share of the window. A call in a chat client is something you
512-;; have *while* reading the room — past about a third of the height it stops
513-;; being that and starts being a video app with a chat box attached.
514-(def ^:private wall-share 0.34)
545+;; The wall's share of the window height.
546+;;
547+;; A third was the first guess and it was wrong: on a 1920x1060 screen it held
548+;; one person to a 440-point tile with fourteen hundred points of empty width
549+;; beside them, which reads as video that will not scale — because in every way
550+;; the eye can tell, it does not. Half leaves the conversation legible under it
551+;; while letting a maximised window actually be worth maximising.
552+(def ^:private wall-share 0.5)
515553
516554 (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols)))
517555
@@ -178,6 +178,17 @@
178 ;; The last thing the media plane failed with, for the line under the controls.178 ;; The last thing the media plane failed with, for the line under the controls.
179 (defonce media-error (atom nil))179 (defonce media-error (atom nil))
180 180
181+;; What to do when this device stops being in a call without having asked to.
182+;;
183+;; The media plane failing is not the server hearing about it: freeq counts a
184+;; participant until an `av-leave` says otherwise. Dropping out quietly leaves
185+;; a ghost in the room, and pressing Join again adds a second one — which is
186+;; how a channel ends up reporting seven people in a call with two.
187+;;
188+;; `frq.state` puts the TAGMSG here. This namespace cannot send one itself
189+;; without depending on the client that owns the connection.
190+(defonce on-dropped (atom nil))
191+
181 ;; Feeds pushed to Vidya, so the ones that stop can be dropped again. Without192 ;; Feeds pushed to Vidya, so the ones that stop can be dropped again. Without
182 ;; this the last frame of someone who left hangs on the wall for the rest of193 ;; this the last frame of someone who left hangs on the wall for the rest of
183 ;; the call.194 ;; the call.
@@ -249,9 +260,11 @@
249 (reset! feeds []))260 (reset! feeds []))
250 261
251 (defn stop-media!262 (defn stop-media!
252- "Leave the media plane and forget the call. Signaling is the caller's to send263+ "Leave the media plane and forget the call, telling nobody.
253- — `leave!` does both, and this is what is left when the server ended it for264+
254- us."265+ For the two cases where the server already knows: it ended the call itself,
266+ or the caller is about to send an `av-leave` of its own. Anything else wants
267+ `dropped!`, or freeq goes on counting a participant who is not there."
255 []268 []
256 (raw-stop)269 (raw-stop)
257 (drop-feeds!)270 (drop-feeds!)
@@ -259,6 +272,18 @@
259 (reset! local-call nil)272 (reset! local-call nil)
260 (reset! media-error nil))273 (reset! media-error nil))
261 274
275+(defn dropped!
276+ "We are out of the call and did not ask to be — the media plane failed, or
277+ the transport went away under it.
278+
279+ Tells whoever registered `on-dropped` first, while the session id and
280+ instance it needs are still here to be read, and only then forgets them."
281+ []
282+ (when-let [announce @on-dropped]
283+ (when-let [call @local-call]
284+ (try (announce call) (catch Exception _ nil))))
285+ (stop-media!))
286+
262 (defn- start-media!287 (defn- start-media!
263 "Dial the SFU for the call we have already joined over IRC.288 "Dial the SFU for the call we have already joined over IRC.
264 289
@@ -374,12 +399,21 @@
374 :has-camera? (not (zero? (raw-status-has-camera)))399 :has-camera? (not (zero? (raw-status-has-camera)))
375 :has-mic? (not (zero? (raw-status-has-mic))))))400 :has-mic? (not (zero? (raw-status-has-mic))))))
376 401
402+ ;; Both of these are the call ending underneath us rather than at
403+ ;; our request, so both have to be announced. A failure keeps the
404+ ;; local call up afterwards so the reason stays on screen — but the
405+ ;; server is told either way, because we are no longer in the call
406+ ;; whether or not the person has read why yet.
377 (= code status-ended)407 (= code status-ended)
378- (stop-media!)408+ (dropped!)
379 409
380 (= code status-failed)410 (= code status-failed)
381- (do (reset! media-error (status-text))411+ (let [why (status-text)
382- (swap! local-call #(when % (assoc % :media :failed)))))412+ call @local-call]
413+ (when-let [announce @on-dropped]
414+ (when call (try (announce call) (catch Exception _ nil))))
415+ (reset! media-error why)
416+ (swap! local-call #(when % (assoc % :media :failed)))))
383 (recur)))))417 (recur)))))
384 418
385 (defn- pump-frames!419 (defn- pump-frames!
@@ -498,20 +532,24 @@
498 ;; A tile narrower than this is not a face, it is a thumbnail of one.532 ;; A tile narrower than this is not a face, it is a thumbnail of one.
499 (def ^:private min-tile 96)533 (def ^:private min-tile 96)
500 534
501-;; And an upper bound, so that one person alone does not become a wall-sized535+;; And an upper bound, so one person alone does not become a wall-sized
502-;; portrait in a maximised window. Generous rather than tight — what actually536+;; portrait. Generous rather than tight — what actually stops tiles growing is
503-;; stops tiles growing is the height budget below, and this is only here so537+;; the height budget below, and this is only here so there is an answer on a
504-;; there is an answer on a screen tall enough not to bind.538+;; screen tall enough that it never binds.
505-(def ^:private max-tile 480)539+(def ^:private max-tile 720)
506 540
507 ;; What a tile costs in height beyond its picture: the name under it, and the541 ;; What a tile costs in height beyond its picture: the name under it, and the
508 ;; gap to the row below.542 ;; gap to the row below.
509 (def ^:private tile-label 22)543 (def ^:private tile-label 22)
510 544
511-;; The wall's share of the window. A call in a chat client is something you545+;; The wall's share of the window height.
512-;; have *while* reading the room — past about a third of the height it stops546+;;
513-;; being that and starts being a video app with a chat box attached.547+;; A third was the first guess and it was wrong: on a 1920x1060 screen it held
514-(def ^:private wall-share 0.34)548+;; one person to a 440-point tile with fourteen hundred points of empty width
549+;; beside them, which reads as video that will not scale — because in every way
550+;; the eye can tell, it does not. Half leaves the conversation legible under it
551+;; while letting a maximised window actually be worth maximising.
552+(def ^:private wall-share 0.5)
515 553
516 (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols)))554 (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols)))
517 555
modified src/frq/state.jolt +11 -0
@@ -788,6 +788,17 @@
788788 :camera? false})]
789789 (irc/tagmsg! c channel (av/join-tags session-id instance)))))
790790
791+(defn announce-leave!
792+ "Tell the room this device is out of a call it did not choose to leave.
793+
794+ freeq counts a participant until an `av-leave` says otherwise, so a media
795+ plane that fails silently leaves a ghost behind — and the next Join adds
796+ another beside it. Registered with `frq.av` at startup, because that
797+ namespace has no connection to send on."
798+ [{:keys [channel session-id instance]}]
799+ (when (and @conn (seq (or session-id "")))
800+ (irc/tagmsg! @conn channel (av/leave-tags session-id instance))))
801+
791802 (defn leave-call!
792803 "Leave the call, telling the room and the SFU both.
793804
@@ -788,6 +788,17 @@
788 :camera? false})]788 :camera? false})]
789 (irc/tagmsg! c channel (av/join-tags session-id instance)))))789 (irc/tagmsg! c channel (av/join-tags session-id instance)))))
790 790
791+(defn announce-leave!
792+ "Tell the room this device is out of a call it did not choose to leave.
793+
794+ freeq counts a participant until an `av-leave` says otherwise, so a media
795+ plane that fails silently leaves a ghost behind — and the next Join adds
796+ another beside it. Registered with `frq.av` at startup, because that
797+ namespace has no connection to send on."
798+ [{:keys [channel session-id instance]}]
799+ (when (and @conn (seq (or session-id "")))
800+ (irc/tagmsg! @conn channel (av/leave-tags session-id instance))))
801+
791 (defn leave-call!802 (defn leave-call!
792 "Leave the call, telling the room and the SFU both.803 "Leave the call, telling the room and the SFU both.
793 804