File Details
FastGuildInvite-v2.1.12
- R
- May 21, 2026
- 5.64 MB
- 389
- 12.0.1+6
- Retail + 2
File Name
FastGuildInvite-FastGuildInvite-v2.1.12.zip
Supported Versions
- 12.0.1
- 12.0.0
- 11.2.7
- 4.4.0
- 3.4.3
- 2.5.5
- 1.15.8
<FGI> FastGuildInvite
[v2.1.12] (2026-05-21) — Settings + main-UI tooltip audit, retail multi-part whisper-echo hide (UI-014), conversation-tab popout suppression, whisper-filter secret-string defense (UI-015)
Retail whisper-filter secret-string defense (UI-015)
User report after extended bulk-invite session: "Hide outgoing whisper echoes" worked fine for the first ~20 invites, then silently stopped working — every subsequent recruitment whisper echoed into chat as To [Recipient]: ... despite the toggle still being on. The user's chat log showed a burst of achievement broadcasts immediately before the regression started.
Same family of bug as v2.1.9's UI-011 (Scan.lua) and v2.1.11's UI-013 (FGI_UnitTooltip.lua). On retail, the CHAT_MSG_WHISPER_INFORM target field can carry a Blizzard "secret string" value when the event ultimately traces back to a protected context (achievement broadcasts, raid warnings, M+ death notifications). fn.hideWhisper was passing that target straight into fn:fullPlayerName(name), whose first call (name:find("%-")) throws "attempt to perform string operation on a secret string value" on the secret. The throw from inside a chat-filter callback leaks the message (filter returns no value, treated as false → message displays), and is consistent with the user's symptom of cumulative degradation after a high-traffic period containing protected-context events.
The defense is the same pattern UI-011 / UI-013 established:
fn.hideWhisper(functions.lua) now pcallsfn.fullPlayerName(name)on retail. If the call throws (becausenameis a secret value), the filter returns false (let the message through) instead of dying — the chat-line for that one event leaks, but the filter survives the rest of the session.- The conversation-tab sweep helper
closeDedicatedWhisperFramesForTargetalso hardened: everychatTarget:lower()op on each chat frame is wrapped in a retail-awaresafeLower(s)helper so a secret-stringchatTargeton any single frame can't throw from inside one of the four scheduledC_Timer.Aftercallbacks.
Classic-family clients skip both pcall overheads — secret strings don't exist on those versions.
Modified: functions.lua — fn.hideWhisper and closeDedicatedWhisperFramesForTarget. New safeLower helper.
Conversation-tab popout suppression
User request: "I have WoW set up to open a new tab on a whisper chat conversation. I want this for real conversations, but FGI's recruitment whispers are leaving a litter of empty tabs even though the echo filter hides the text. Can the tab not open for FGI's whispers but still open for real responses?"
The popout is WoW's standard behaviour when Social options → Chat → Whisper mode → Conversation is set — every new whisper target gets its own dedicated chat frame on first WHISPER_INFORM. The echo filter hides the chat-line text but the tab itself remains, so a bulk-invite session leaves dozens of empty tabs.
Attempted two designs before landing on the working one:
C_Timer.After(0, ...)after each send (functions.luafn:sendWhisper, pre-fix) — capturehadDedicatedFramebefore sending, schedule a next-tick close if no frame existed. Failed on retail: the popout's frame setup wasn't complete by the next tick, andFCFManager_GetDedicatedFramereturned nil so the close found no frame.hooksecurefunc("FCF_OpenTemporaryWindow", ...)— hook the frame-creation API directly so the close runs synchronously with creation. Failed because modern retail (TWW 11.x) routes whisper popouts through a different code path; the hook never fires for retail's popout.- (Working) Iterate
CHAT_FRAMESdirectly. Drop the hook, drop the time-of-send capture. Whenfn.hideWhispersuppresses an echo, callfn.suppressConversationTabFor(target). That helper does an immediate sweep plus three scheduled sweeps (0 s, 0.1 s, 0.5 s) ofCHAT_FRAMES, finding any frame withchatType == "WHISPER"+chatTarget == ourTargetand callingFCF_Closeon it. The four sweeps catch the popout regardless of when retail's chat pipeline creates the frame. The match predicate skipsDEFAULT_CHAT_FRAME(nochatType) and user-created custom frames (multiple chat-type registrations rather than a singlechatType), so only auto-generated popout tabs match.
Real responses from the recruit go through CHAT_MSG_WHISPER (not _INFORM), don't refresh the deadline, and don't trigger the sweep. Their popout sticks. Recruitment whispers suppressed; real conversations preserved.
Known trade-off: if the user is already in an active conversation with the recruit (pre-existing tab for that exact name), FGI's sweep closes that tab too — both share the same chatType+chatTarget match. For recruitment workflow this is rare (recruits are strangers; if you're already chatting with them, you wouldn't normally re-whisper them through FGI), so the simpler design without "had pre-existing tab" capture was kept. Adding the capture is one DB field if a user reports the trade-off matters.
Gated on DB.realm.sendMSG so users who WANT the popout for every whisper (echo suppression off) keep it.
Modified: functions.lua — fn.suppressConversationTabFor, closeDedicatedWhisperFramesForTarget (helper, internal). fn.hideWhisper calls the suppressor on each suppressed echo. Updated the sendMSG toggle's desc in GUI/SettingsPanel.lua to mention the tab-closing behaviour.
Retail multi-part whisper-echo hide fixed (UI-014)
User report: with "Hide outgoing whisper echoes" enabled on retail, a recruitment whisper long enough to split into two SendChatMessage calls (>255 bytes after NAME / GUILD / GUILDLINK expansion) only hid the first echo — the second echo leaked into the chat frame.
The v2.1.8 design queued one entry per split chunk in addon.removeMsgList[fullName] and the CHAT_MSG_WHISPER_INFORM filter popped the head 1:1 with arriving echoes. Reading the code, the queue logic is correct (push msg1, push msg2; pop msg1, pop msg2), and single-part whispers worked on every client. Repro on retail showed the second echo slipping past with the queue still holding its entry — root cause not pinned to a specific line, but plausible candidates are a retail-specific filter-invocation timing difference, or the CHAT_MSG_WHISPER_INFORM event dispatching differently for the second of a paired-send burst.
Rather than continue debugging the queue's failure mode, switched to a deadline approach that's robust against any miscount:
addon.removeMsgList[fullName]is now aGetTime()deadline (number), not a list of messages.- Every
SendChatMessagecall infn:sendWhisperrefreshes the deadline toGetTime() + FGI_WHISPER_HIDE_WINDOW(10 s). fn.hideWhisperchecksGetTime() < deadline. Within the window: hide. After: fall through.
Trade-off documented in functions.lua: a manual whisper to the same target within 10 s of FGI's whisper would also be hidden. For a recruitment workflow this is rare (the recruit is being whispered FROM FGI; the user is unlikely to manually whisper them inside that window).
Modified: functions.lua — fn.hideWhisper, fn:sendWhisper. New constant FGI_WHISPER_HIDE_WINDOW = 10. No DB schema change; same sendMSG toggle, same removeMsgList lifecycle from an external perspective.
Settings panel tooltip audit (5 fixes)
User report: the "Level range priority" dropdown on the General tab had no hover tooltip (the desc was rendered as a permanent inline label via descStyle = "inline", so hovering the chevron did nothing). Audit of GUI/SettingsPanel.lua found four other widgets missing their desc entirely:
levelRangePriority(GUI/SettingsPanel.lua:723) — restructured to match the Guild-tab pattern. Added a newlevelRangePriorityHeader(FGI_TooltipHeader section header) above the dropdown, carrying the full Strip-vs-Filters explanation. The dropdown itself was renamed to "Source" with a short desc pointing at the header above. User feedback was that the Strip-vs-Filters explanation belonged on a section header (like every other Guild-tab section header carries its topic's explanation), not on the widget chevron.autoWelcome(Guild → Welcome on join) — added a desc explaining it posts the welcome message to guild chat on member join, fires only for joins while you're online.autoWelcomeWhisper(Guild → Welcome on join) — added a desc explaining the private whisper, pairs with or replaces the guild-chat welcome.setNote(Guild → Notes on invite) — added a desc explaining the public-note auto-write, the 31-char cap, the Retail restriction.setOfficerNote(Guild → Notes on invite) — added a desc explaining officer-note auto-write, the rank-permission requirement, the 31-char cap.
Audit confirmed every other option in the panel (~76 widgets across General, Messages, Guild, Bnet, Appearance, Hotkeys, Advanced, Debug) already had appropriate desc text.
Main-UI help "i" tooltip audit (3 fixes)
Audit of the per-tab help text in TAB_HELP (GUI/MainWindow.lua:59-212) against the actual tab implementations found drift on two tabs:
- Scan tab — help text described strip controls as
>>/+(N)/Clear/Mode, missing theSel Alland+(N)selmulti-select buttons (added in v2.1.5). Counter description listed F/S/A/X, missing the D (Declined) counter. Lvl readout description referenced "Default level range priority" setting; actual setting name is "Level range priority" (no "Default" prefix). Queue rows description didn't mention the multi-select checkbox column. Fixed all four. - Anti-Spam tab — help text said entries auto-prune after the period set in Settings → Main → 'Clear DB after'. Actual setting name is "Anti-spam expiry". Fixed.
- CustomScan / Filters / Blacklist / History / Statistics / QuietZones / Announce — verified accurate against current implementations.
Compact-tray tooltip audit (3 fixes)
Audit of Locale/enUS.lua tooltip strings consumed by Modules/compactFrame.lua found two cases where the tooltip described the opposite of the code's behaviour:
L["rowDeclineTooltip"](Locale/enUS.lua:176) — claimed Decline only added the player to Anti-Spam if "Remember skipped" was enabled. Verified at functions.lua:1684-1697: the Decline path (fn:invitePlayer(noInv=true)) always records to anti-spam, unconditionally. The comment at line 1689 confirmsrememberSkipped"never actually fired on the Skip button, only here on Decline." Rewrote the tooltip to describe the actual behaviour.L["compactToggleTooltip"](Locale/enUS.lua:190) — text described toggling INTO compact mode ("Click to shrink the main window..."). The+button it labels lives only on the compact tray and switches BACK to the full window (the opposite direction). Rewrote.L["rowSkipTooltip"](Locale/enUS.lua:175) — accurate default-case description but missing the conditional. Compact tray Skip handler at Modules/compactFrame.lua:702-721 does gate anti-spam recording onDB.global.rememberSkipped. Added the conditional mention.
Also: compact-tray help "i" (Modules/compactFrame.lua) said "Hover the F:S:A:X counters" — actual counter strip has five letters including D (Declined). Updated to F:S:A:X:D.
Other compact-tray tooltips (scan / invite strip buttons, row Invite, row Blacklist, announce horn, settings gear, close X, counter-strip hover) all verified accurate against current implementations.
Non-English locale files (ruRU, deDE, frFR, esES, esMX, ptBR, itIT, koKR, zhCN, zhTW) keep their existing strings until a translator updates them — standard FGI pattern per CLAUDE.md.
[v2.1.11] (2026-05-20) — Retail unit-tooltip secret-value crash on dungeon zone-in (UI-013)
Retail FGI_UnitTooltip.lua secret-value crash fixed (zoning into a dungeon / hovering instanced content)
User report: zoning into a retail M+ dungeon stacked the following error twice on the way in:
bad argument #1 to 'UnitIsPlayer' (Usage: local result = UnitIsPlayer([unit, partyIndex]).
Secret values are only allowed during untainted execution for this argument.)
[fastguildinvite/Modules/FGI_UnitTooltip.lua]:48: in function <...UnitTooltip.lua:43>
[fastguildinvite/Modules/FGI_UnitTooltip.lua]:97: in function <...UnitTooltip.lua:96>
[C]: in function 'securecallfunction'
[Blizzard_SharedXMLGame/Tooltip/TooltipDataHandler.lua]:67: ...
[Blizzard_GameTooltip/Mainline/GameTooltip.lua]:997: in function 'SetWorldCursor'
[Blizzard_UIParent/Mainline/UIParent.lua]:1274: ...
Same family of bug as v2.1.9's UI-011 Scan.lua secret-string crash, different vector. On retail Blizzard wraps certain unit tokens in protected "secret values" (M+ portals, instanced creatures, cinematic NPCs, the dungeon zone-in fly-in cursor, etc.). Our TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, ...) callback at Modules/FGI_UnitTooltip.lua:96 runs from securecallfunction for every tooltip event, including the secret-token ones. The first unit-API call in enrich() — UnitIsPlayer(unit) at line 48 — throws "Secret values are only allowed during untainted execution for this argument" because the secret token can't be passed to a unit-API call from our addon-tainted context.
The if not unit then return end guard above the failing call doesn't trip because the secret-value is truthy (it's not nil, it's a sentinel object that Blizzard renders as <no value> in BugSack dumps but is internally a magic table).
Fix: pcall every unit-API call on retail. If any of UnitIsPlayer / UnitIsInMyGuild / UnitName / UnitLevel raises on a secret token, the wrapper returns false / nil / 0 and enrich() bails silently — the same outcome it would have if the unit weren't a guildie. Classic-family clients don't have secret unit tokens so they skip the pcall overhead and call the API directly.
Modified: Modules/FGI_UnitTooltip.lua — four safeUnit* wrappers around the four unit-API calls, gated on gv.isRetail. No behaviour change on guildie tooltips (they still get the "FGI: joined N days ago" / "Was level X at join" lines on retail and classic alike).
Architectural note: Both UI-011 and UI-013 are instances of the same pattern: a Blizzard secure-execution surface (CHAT_MSG_SYSTEM, TooltipDataProcessor) hands our addon a protected value, and a unit-/string-API call against that value throws. The defense is always the same — pcall the call on retail, silent bail on failure. Any future module that consumes secret-eligible data (unit tokens, system message strings, item links from secure events) needs the same wrapper. Pattern logged in docs/DEV_NOTES.md.
[v2.1.10] (2026-05-20) — Announce keybind, icon hitbox parity across main window + compact tray
Third addon-managed keybind: "Fire announce profiles"
Settings → Main → Hotkeys now has three rows: Invite, Next-scan, and the new Announce. Same addon-managed OnKeyDown listener architecture introduced in v2.1.8 — DB is the source of truth, WoW's binding system is not touched.
- New DB slot:
DB.global.keyBind.announce(per-character, defaults to nil) in FGI_Core.lua:652 - Dispatch in the
FGI_KeybindListenerOnKeyDownhandler at FGI_Core.lua:939-941 — matches the captured canonical key string againstkb.announceand callsaddon.announce:Send()on a hit. Hardware-event taint context is preserved (same path as invite / next-scan dispatch), soSendChatMessagefrom inside the announce flow stays inside the secure-execution model. - New keybinding row in GUI/SettingsPanel.lua:1496-1517 —
announceKeytable parallel to the existinginviteKey/nextSearchKeyrows.setcallback writes DB only (noSetBindingClick/SaveBindings). - Hotkeys header description updated to mention three actions.
Icon hitbox parity across main window + compact tray
User report: settings gear was only clickable in the centre, not on the edges. Two root causes addressed:
- Blizzard
Interface\Icons\*textures render with ~8% transparent padding on every edge, so the visible art only covers ~84% of the button it lives on. Users perceive that padding as the icon's edge but it isn't part of the click target — only the central ~17×17 of a 20×20 button "felt" clickable. Fixed by croppingSetTexCoord(0.08, 0.92, 0.08, 0.92)on theInterface\Icons\textures so the art fills the button edge to edge. - The button hit-rect matched the visible 20×20 box exactly. Even with the texture filling the box, the click area was tight. Fixed by
SetHitRectInsets(-2, -2, -2, -2)on every bottom-row / title-row icon, giving 2 px of click slop in every direction. The compact tray's title-row icons sit ICON_GAP px apart and the main window's row icons sit 3 px apart, so the 2 px slop stays inside the gap and can't collide with neighbour click areas.
Both fixes applied to:
- Main window (GUI/MainWindow.lua) — gear, announce horn, help "i", compact-mode minus. Gear and horn get both fixes (Blizzard Icons atlas); help-i and compact-mode minus only get the hit-rect inset because they live in
Interface\Common\andInterface\Buttons\respectively and don't carry the 8% padding. - Compact tray (Modules/compactFrame.lua) —
makeTitleIconandmakeRowIconhelpers now apply the hit-rect inset unconditionally and crop TexCoord conditionally via a newisPaddedIconTexture()check on the texture path. The closeBtn (X glyph, no texture) also gets the hit-rect inset so it feels the same to click as the gear / help / announce / expand buttons next to it.
Net effect: every clickable icon in FGI now has a hit area that matches the visible icon plus 2 px of slop on every side. The gear and horn icons look subtly larger because the TexCoord crop removes the padding.
Modified:
- FGI_Core.lua —
keyBind.announcedefault, OnKeyDown announce dispatch, header comment updated - GUI/SettingsPanel.lua —
announceKeyrow, header desc updated - GUI/MainWindow.lua —
SetHitRectInsetson helpIcon + compactModeIcon - Modules/compactFrame.lua —
isPaddedIconTexturehelper, hit-rect inset + conditional TexCoord crop inmakeTitleIconandmakeRowIcon, hit-rect inset on closeBtn
[v2.1.9] (2026-05-20) — Retail Scan.lua secret-string crash fix
Retail Scan.lua secret-string crash fixed (CHAT_MSG_SYSTEM during M+ / raid)
User report (post-v2.1.8 push): 19 stacked errors during a Mythic+ dungeon on retail:
attempt to perform string conversion on a secret string value (execution tainted by 'FastGuildInvite')
[FastGuildInvite/Scan.lua]:22: in function <FastGuildInvite/Scan.lua:11>
[FastGuildInvite/Scan.lua]:208: in function <FastGuildInvite/Scan.lua:132>
Root cause in Modules/Scan.lua:132-208 — the pausePlayFilter:SetScript("OnEvent", ...) handler that intercepts CHAT_MSG_SYSTEM (looking for invite-decline / not-found patterns) passed the raw msg straight into playerHaveInvite(). On retail, CHAT_MSG_SYSTEM messages can carry protected "secret string" name references (M+ death notifications, achievement broadcasts, raid-warning targets, etc.). The first strfind / strsub call inside playerHaveInvite on those messages throws the "attempt to perform string conversion on a secret string value" error and marks FastGuildInvite as the tainting addon. The handler fires for every CHAT_MSG_SYSTEM event, so during a M+ run with dozens of broadcasts the same error stacked 19 times.
Fix: pcall the playerHaveInvite() call on retail. Normal (non-secret) CHAT_MSG_SYSTEM messages still flow through to the invite-decline / not-found matcher as before; secret-string messages get silently dropped (we don't care about them — they're never invite-related). Classic-family clients skip the pcall overhead since they don't have secret strings on CHAT_MSG_SYSTEM.
Latent bug noted but not fixed in this pass: the retail-specific UI_ERROR_MESSAGE branch at Modules/Scan.lua:138-200 calls pcall(playerHaveInvite, msg) while msg is still nil (the msg = text assignment happens later at line 203, after the retail-only return). That branch silently no-op's on retail, but invite-related events on retail go through CHAT_MSG_SYSTEM rather than UI_ERROR_MESSAGE anyway, so the broken branch hasn't caused user-visible issues. Flagged for cleanup in a later release alongside a wider Scan.lua handler refactor.
Modified: Modules/Scan.lua — wrapped the fall-through playerHaveInvite(msg) call in a retail-aware pcall.
[v2.1.8] (2026-05-20) — Member-history tooltips, blacklist popup restored, multi-select invite gate fix, addon-managed keybinds, whisper-echo mute fixed on retail, scan progress-bar paints 100%, Communities-menu taint fix, UI minimum-size overlap fix, Modules/ reorg
Member-history tooltips
FGI now persists per-member metadata when a player joins your guild and surfaces it in two tooltip contexts: WoW's native unit tooltip (mouseover, guild roster panel, unit frames) and a brand-new standalone tooltip when you hover a player name in chat. Both default-on, each toggleable independently in Settings → Guild → Member-history tooltips.
Storage layer in Modules/FGI_MemberHistory.lua (new file). Schema:
DB.factionrealm.memberHistory[normalizedName] = { joinedAt = epoch, lvlAtJoin = number }. Populated from FGI_Core.lua's existing LibGuildRosterOnMemberJoinedcallback — wrote-through immediately forjoinedAt, deferred 2.5 s for the level read so the guild roster has time to settle (mirrors the 2.5 s welcome-message delay in the same callback).addon.MemberHistory:get(name)is the public lookup the tooltip files use.Native unit tooltip enrichment in Modules/FGI_UnitTooltip.lua (new file). Hooks
GameTooltip:SetUnitvia the modernTooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Unit, ...)on retail and the legacyOnTooltipSetUnitscript hook on Classic / TBC / Wrath / Cata. Adds 1–2 lines to the native tooltip when the unit is a current guildie with member history:FGI: joined N days ago, and (when their level has grown)Was level X at join (now Y). Pre-FGI members get no extra lines (silent — we have no data on them).Standalone chat-hyperlink tooltip in Modules/FGI_ChatTooltip.lua (new file). Hooks
OnHyperlinkEnter/OnHyperlinkLeaveon every chat frame (and re-hooks atPLAYER_LOGINto catch frames created late). Parses|Hplayer:Name-Realm:...|hlinks and builds a full standalone tooltip — class-coloured name, level / class / rank, online status + zone, public note, plus FGI's member history, blacklist status, anti-spam history, and ex-member flag. Uses a dedicatedFGI_ChatTooltipFrame(ownGameTooltipTemplateinstance, not the sharedGameTooltip) so addons that hover a unit while the user hovers a chat link can't collide. Anchored beside the chat frame the hyperlink lives in, on whichever side of the screen has more room — keeps the chat content readable. The initial cut of this module missed aSetOwner(UIParent, "ANCHOR_NONE")call beforeClearLines/AddLine/Show, so the tooltip rendered as an empty invisible frame and the hover felt like it did nothing. Fixed mid-pass; the rest of the module is unchanged.Two new toggles in GUI/SettingsPanel.lua under the new
Member-history tooltipsheader in the Guild sub-page:showMemberTooltipandshowChatTooltip, both default on.DB defaults in FGI_Core.lua —
showMemberTooltip = true,showChatTooltip = trueadded to theglobaldefaults block.Backfill policy: pre-v2.1.8 members get no history entry (FGI only knows about joins that fire
OnMemberJoinedwhile it's loaded). Tooltips silently render no extra lines for them. A future release could seed afirstObservedAtrow by iterating the live guild roster on first run; intentionally deferred.
Blacklist popup restored (FGI_BLACKLIST / FGI_BLACKLIST_CHANGE ghost-name crash and silent no-op)
User report: blacklisting any player produced attempt to index field 'FGI_BLACKLIST' (a nil value) at functions.lua:797. Root cause: the v1 → v2 strip removed two StaticPopupDialogs definitions (FGI_BLACKLIST and FGI_BLACKLIST_CHANGE) that lived in the deleted blackList.lua, but left the three call sites pointing at the dead names. The FGI_BLACKLIST site (guildKick → StaticPopupDialogs["FGI_BLACKLIST"].add(name)) crashed hard because .add was a method on the deleted dialog. The two FGI_BLACKLIST_CHANGE sites (chat right-click menu legacy fallback in FGI_Core.lua:142, and the /fgi blacklist <name> slash command at FGI_Core.lua:1004) silently no-op'd because StaticPopup_Show returns nil for undefined dialog names — players using the non-fast-blacklist path got no confirmation popup at all.
Fix in functions.lua — restored StaticPopupDialogs["FGI_BLACKLIST"] with the same kick / skip semantics the v1 definition had:
textrewritten at show-time with the head-of-queue name ("Blacklisted player %s is in your guild!")button1 = "Kick"callsGuildUninvite(name)via pcall, marks the name indata2(session "already prompted" set), pops it fromdata, shows the next queued name if anybutton2 = "Skip"does the same minus the kick — still records the name indata2so we don't re-prompt this sessionguildKick(name)now dedupes against bothdata(active queue) anddata2(session set) before appending; shows the popup if it was the first add- Forward-declared
local showNextKickPromptso the OnAccept / OnCancel callbacks can call it for queue-drain (defined after the dialog table to keep the dialog literal compact)
Fix in FGI_Core.lua:142 (chat right-click "FGI - Black List" with fastBlacklist off) and FGI_Core.lua:1014 (/fgi blacklist <name> slash command with no reason and fastBlacklist off) — both call sites now route through the modern addon.UI.ShowBlacklistConfirm(entry, onDone) (defined in GUI/UI.lua) which is the v2 reason-input dropdown. The chat-menu site preserves the original "advance the scan queue if the blacklisted player was the head of the queue" semantics by passing an onDone callback that calls fn:invitePlayer(true). The slash-command site has no scan-advance need, so it passes just the name.
Why the popup was ever called from the slash command path: the v1 design used StaticPopupDialogs["FGI_BLACKLIST_CHANGE"] as a "you blacklisted X without giving a reason — want to enter one now?" prompt. Same idea as the modern UI.ShowBlacklistConfirm, just rendered differently. Routing the slash command to the modern path keeps the UX coherent — typing /fgi blacklist Bob with no reason and fastBlacklist off now opens the same reason-input dropdown the right-click chat menu opens, instead of silently no-op'ing.
Modified files:
- functions.lua —
StaticPopupDialogs["FGI_BLACKLIST"]definition +showNextKickPrompthelper +guildKickrewrite - FGI_Core.lua — two call sites swapped from
StaticPopup_Show("FGI_BLACKLIST_CHANGE", ...)toaddon.UI.ShowBlacklistConfirm(...)
Keybinds migrated to addon-managed OnKeyDown listener (bypass WoW's binding system entirely)
User report (v2.1.8 regression, not seen on v2.1.7's fix): the Hotkeys keybind worked after clearing and re-adding in Settings → Main → Hotkeys, but stopped firing once the user closed the settings panel. Re-binding made it work again until the next panel close. Environment-specific — the maintainer's local test of v2.1.7 didn't repro this; another player did.
Root cause was almost certainly AceConfigDialog's keybinding widget interacting with Blizzard's secure binding system on hide. The widget wraps Blizzard's keybinding primitives, and at panel-close it can run a binding-state restore that resets the in-memory binding for the captured key. SetBindingClick from the Hotkeys row's set callback set the binding correctly, SaveBindings persisted it to bindings.wtf — but the close-time state restore wiped the in-memory binding for the rest of the session. /reload brought it back via OnInitialize re-applying; within the same session, only re-binding fixed it.
A mid-cycle defensive-reapply attempt (3 hooks on OnInitialize / PLAYER_ENTERING_WORLD / AceConfigDialog:Close) covered the reported trigger but left the system fundamentally fragile — anything in WoW or another addon that touches the binding state can still drop us, and we'd need to keep adding hooks for every new trigger discovered.
Fix in this release: stop using WoW's binding system entirely. FGI keybinds are now dispatched by an OnKeyDown handler on a hidden frame the addon owns. The dispatch path:
- Storage (unchanged):
DB.global.keyBind = { invite = "F6", nextSearch = "SHIFT-F7" }. AceConfigDialog's keybinding widget captures user input as before and writes the canonical "ALT-CTRL-SHIFT-KEY" string into DB; thesetcallbacks in GUI/SettingsPanel.lua only write to DB now and do not callSetBindingClick/SetBinding/SaveBindings. - Listener in FGI_Core.lua: a hidden
FGI_KeybindListenerframe parented toUIParentwithEnableKeyboard(true)andSetPropagateKeyboardInput(true). The OnKeyDown handler reconstructs the canonical key string with modifier prefixes (ALT-CTRL-SHIFT-in alphabetical order, matching the AceConfigDialog widget's format), matches againstDB.global.keyBind.{invite, nextSearch}, and dispatches tofn:invitePlayer()orfn:nextSearch()on a hit. Propagation stays on so the game's normal key handling continues afterward (movement / spells / chat keys behave unchanged). - Suppression: when
GetCurrentKeyBoardFocus()returns non-nil — chat EditBox while typing, AceConfigDialog keybinding widget while capturing, any other addon's input field — the handler early-returns. The user can type and rebind without firing the action. - Hardware-event taint:
OnKeyDownruns in a hardware-event taint context, the same wayFrame:OnClickdoes, so calling protected functions likeC_GuildInfo.Invitefrom inside the handler stays within that context and the secure-execution model accepts the call. This is the same mechanismSetBindingClickwas using internally viaClick()dispatch — we just do it directly. - Migration cleanup: OnInitialize iterates
GetBinding(i)once per session and clears any WoW bindings that still point atFGI_CompactInviteBtn/FGI_CompactScanBtnfrom v2.1.7-eraSetBindingClickcalls. Without this, the post-upgrade first session would see both the stale WoW binding and our new listener fire for the same keypress, double-invoking the action. Idempotent — subsequent loads find nothing.
What's gone:
- The previous
addon.reapplyKeybindshelper and its three hook registrations (OnInitialize /PLAYER_ENTERING_WORLD/AceConfigDialog:Close) — no longer needed because nothing in WoW's binding system is involved in our keybinds anymore. SetBindingClick/SetBinding/SaveBindingscalls from the settings-panel keybindsetcallbacks — DB is now the only persistence path.
WoW's native Key Bindings panel never shows FGI bindings (matches the v2.1.5 design — bindings live inside the addon's Hotkeys panel only).
Modified files:
- FGI_Core.lua — OnInitialize cleanup block +
FGI_KeybindListenerframe setup;addon.reapplyKeybindshelper removed - GUI/SettingsPanel.lua — both keybind
setcallbacks reduced to DB writes only
"Hide outgoing whisper echoes" toggle now actually hides them on retail (fn.hideWhisper text-match was too strict)
User report: the "Hide outgoing whisper echoes" toggle in Settings → Main → Chat noise is checked but FGI's recruitment whispers still echo to chat as To [Player]: ... on retail. The user confirmed it isn't a third-party-addon conflict — they disabled everything except FGI + Ace3 and the echoes still leaked through.
Root cause in functions.lua:1485 (fn.hideWhisper, the registered CHAT_MSG_WHISPER_INFORM filter): the v1.x logic was an exact text-match between the event's message text and the pre-send copy stored in addon.removeMsgList[key]. Only when both matched would the filter return true (suppress the echo). On Classic the post-send event text equals the pre-send stored text byte-for-byte, so the match succeeded reliably. On retail the chat pipeline can normalise the event text in subtle ways (hyperlink whitespace, special-character handling, etc.) so the comparison silently failed for every echo despite the queue being correctly populated. Echo through.
Fix: drop the text comparison; match on target name only.
fn.hideWhispernow resolves the canonicalfullPlayerName(name)key from the event's target arg, looks upaddon.removeMsgList[key], and if there's at least one queued message, pops the head and returnstrue. Multi-part whispers (split byfn:messageSplitfor messages > 255 bytes) are inserted in order and the echoes arrive in the same order, so head-pop drains the queue 1:1 with the echoes for that target.- Surgical scope is preserved:
removeMsgListis keyed by the names FGI is currently sending to (populated infn:sendWhisperimmediately before eachSendChatMessage/C_ChatInfo.SendChatMessagecall), and entries are cleared by the head-pop as echoes drain. Manual whispers to players FGI isn't currently tracking still echo normally. - No DB schema change; same
sendMSGtoggle, sameremoveMsgListlifecycle. Only the filter's accept-or-reject decision changed.
Modified: functions.lua — fn.hideWhisper body.
Scan-progress bar now paints at 100% on completion (was disappearing prematurely)
User report: the orange scan-progress fill in the bottom status bar "pops up, then goes away" — appears during scanning, then disappears the moment the scan finishes without ever showing the fill at 100%. Reads as a glitch.
Root cause in GUI/MainWindow.lua:279 (MainWindow:SetScanProgress): the hide gate was not total or total <= 0 or not done or done >= total. The done >= total clause meant the moment the last query's response came in (incrementing progressDone to equal progressTotal), the texture got hidden — before any tick painted the bar at the 100% fraction. The visible sequence was effectively 80% → hidden, with the 100% paint never happening because the only call site (MainWindow:RefreshStatusBar driven by ScanTab.Refresh) only fires AFTER progressDone updates, by which point the gate had already kicked in.
Fix in GUI/MainWindow.lua:
SetScanProgress: hide gate relaxed tototal <= 0 or done <= 0(hide only when no scan has been started this session, e.g. after Clear). Addedmath.min(1, ...)clamp on the fraction sodone > total(shouldn't happen but defensive) paints at 100% rather than overflowing. The bar now paints at 100% when the final query completes and stays at 100% until the next scan starts.RefreshStatusBar: added a "Scan complete" status-text branch whenonScan and total > 0 and done >= total. The user seesv2.1.8 | Scan complete | 5 / 5 queriesalongside the full bar after their scan finishes, rather than the bar vanishing and the text reverting to just the version string.- The bar clears under the same conditions as before: switching away from the Scan tab (the
onScangate fails), starting a new scan (the newprogressTotaltriggers a 0% repaint that fills back up), or clicking Clear (progressTotal = 0→ bar hides).
Modified: GUI/MainWindow.lua — SetScanProgress and RefreshStatusBar.
UI minimum-size overlap fix (main-window Scan tab + compact tray)
User report: at default size, both the main-window Scan tab and the compact tray showed widget overlap — specifically, the F:n S:n A:n X:n D:n counters bleeding into adjacent widgets. Pre-v2.1.8 minimum-size constants hadn't been updated as new strip widgets piled in over the v2.0+ feature cycle.
Scan tab fix in GUI/Tabs/Scan.lua:
- Counters fontstring had only a LEFT anchor (
LEFTtomodeDD's RIGHT) and extended rightward unbounded. At typical 2-3 digit values, the text reached x≈700 px while the RIGHT-anchoredlvlContainer(120 px wide) started at x≈694 px (strip_width 820 − 126) — overlapping by ~26 px on every scan with any activity. - Added a
RIGHTanchor on counters pinning tolvlContainer's LEFT − 8 px. The fontstring now self-clips at the level-readout boundary regardless of width. - Bumped
ScanTab.MIN_WIDTHfrom 840 to 900 so typical 2-3 digit counter values fit comfortably without clipping at the new safety boundary. Worst-case 4-digit values still clip cleanly at the boundary instead of overlapping the Lvl labels.
Compact tray fix in Modules/compactFrame.lua:
- The 32-wide invite button held a
+(N)label anchored CENTER. When the queue count grew past single digits the label extended past the button's frame in both directions (+(99)≈ 42 px wide on a 32 px button, overflowing 5 px each side). The LEFT overflow crossed into the counter-area boundary 6 px to its left — visual overlap between the button label and the counter text. - Bumped invite button width from 32 to 48 px (matches the main UI's
INV_BTN_Wconstant) so+(NNN)labels fit inside the frame without overflow. - Moved counters'
RIGHTanchor from-182to-198to clear the wider invite button while keeping the 6-px safety gap to its left edge. - Bumped
MIN_WIDTHfrom 300 to 380 so the counter area still accommodates typical 2-digit "F:99 S:99 A:99 X:99 D:99" text after the wider right-side reservation. 3-4 digit pathological values still clip cleanly inside the counter area as before; they no longer collide with the invite button.
Modified files:
- GUI/Tabs/Scan.lua —
ScanTab.MIN_WIDTHconstant +countersRIGHT anchor wired afterlvlContainerexists - Modules/compactFrame.lua —
MIN_WIDTHconstant, invite-button width, counters' RIGHT anchor offset, comment-block update
Multi-select invite hardware-event-gate bug fixed
User report: clicking the +(N)sel button on the Scan tab to invite multiple checked players produced a stream of [ADDON_ACTION_BLOCKED] AddOn 'fastguildinvite' tried to call the protected function 'Invite()' errors. The first invite would fire correctly, every subsequent one in the same click frame was refused.
Root cause: the v2.1.5 design (GUI/Tabs/Scan.lua:477-493 pre-fix) iterated every selected player in a single OnClick handler and called fn:invitePlayer(false, idx) synchronously for each one. WoW's protected-function gate requires a real hardware event (mouse click or keypress) for each call to C_GuildInfo.Invite (retail) or GuildInvite (Classic-family). The user's mouse click is one hardware-event credit — consumed by the first invite. Every invite after that in the same click frame fails the gate and fires ADDON_ACTION_BLOCKED. Timers don't help (timer callbacks aren't hardware events), coroutines don't help, and the same constraint applies on every WoW version. The feature as designed was unbuildable — there is no API path that fires N invites from a single user gesture.
Fix in GUI/Tabs/Scan.lua:474-503 (the +(N)sel button's OnClick handler): the button now invites one selected player per click, scanning the candidate list from the tail forward so the highest-index selected row goes first (invite removes the row, so taking from the tail keeps subsequent click-target indices stable for the rows we'll fire on the next click). The selection state is decremented per click (deselect-by-name before the invite call), so the live counter in the button label tracks "how many invites left to fire."
Same hardware-event-per-invite reality as the compact tray's regular +(N) button. The selection feature is still useful — the user checks the rows they want, hits the button repeatedly, and the counter ticks down — but each invite costs one click, on every version. The button's label is now "Invite next selected" and the tooltip explains the one-per-click constraint.
Modified: GUI/Tabs/Scan.lua — +(N)sel button OnClick and tooltip body.
Communities-menu registration removed (root-cause fix for SetGuildRankOrder ADDON_ACTION_FORBIDDEN)
User report: opening the modern Communities guild roster panel, opening a member's rank dropdown, and picking a new rank produced [ADDON_ACTION_FORBIDDEN] AddOn 'FastGuildInvite' tried to call the protected function 'SetGuildRankOrder()'. FGI never appears in the stack trace — it's entirely Blizzard_Communities/GuildRoster.lua and Blizzard_Menu/Menu.lua — but the taint check names FGI as the addon that polluted the menu's secure tree.
Own-it framing. The bug isn't "Blizzard's rank flow taints us mid-call"; it's "FGI registered Menu.ModifyMenu callbacks against the two menus that uniquely contain protected-call subtrees, and that registration was a mistake." MENU_UNIT_COMMUNITIES_GUILD_MEMBER and MENU_UNIT_COMMUNITIES_WOW_MEMBER are the only menus in the v2.0+ MENU_TAGS list where rank-change dropdowns live as child submenus. Anything FGI's callback does inside those menus — adding a single rootDescription:CreateButton("FGI") is enough — writes addon-taint into the menu's root description, and every child submenu inherits it, including the rank-change dropdown whose Blizzard code then can't call SetGuildRankOrder() cleanly. There's no safe way to modify a Blizzard menu that contains protected-call subtrees; the only correct action is to not modify those menus.
Fix: both Communities tags removed from Modules/FGI_ChatMenu.lua's MENU_TAGS. FGI's chat-menu callback no longer runs in the Communities guild-roster context, so its taint is never written into a menu that hosts a rank-change dropdown. We're not "in the flow" anymore — by construction. FGI integration is preserved everywhere else: chat (MENU_UNIT_CHAT_ROSTER, the original ask), party (MENU_UNIT_PARTY), raid (MENU_UNIT_RAID_PLAYER / MENU_UNIT_RAID), friends list (MENU_UNIT_FRIEND), the classic-style guild panel (MENU_UNIT_GUILD_MEMBER), target (MENU_UNIT_TARGET), focus (MENU_UNIT_FOCUS), and enemy player (MENU_UNIT_ENEMY_PLAYER).
Cost: no FGI submenu when right-clicking a member in the modern Communities guild panel. Trade-off is correct — adding it back via Menu.ModifyMenu would re-introduce the taint vector for any officer with rank-change permissions. If users miss the Communities right-click integration enough to justify it, a future release could mimic the submenu via a custom widget that doesn't touch the secure menu tree; deferred until there's demand.
Modules/ reorganisation
The addon root had grown to 18 .lua files mixing bootstrap, core, hooks, persistent stores, and feature modules. Reorganised into a two-tier layout:
- Root (bootstrap / foundation):
init.lua,FGI_Constants.lua,FGI_Compatibility.lua,FGI_APICompat.lua,FGI_Core.lua,functions.lua. These define the addon namespace, version detection, API wrappers, and the core invite / scan flow that everything else builds on. Modules/: 13 files moved here — every feature module, hook, persistent-store module, and one-shot utility. Moved files:Announce.lua,compactFrame.lua,customInterface.lua,debug.lua,dump.lua,FGI_ChatMenu.lua,FGI_ChatTooltip.lua(new),FGI_MemberHistory.lua(new),FGI_ScanGroups.lua,FGI_UnitTooltip.lua(new),history.lua,intro.lua,Scan.lua.- Unchanged:
GUI/,Libs/,Locale/,fonts/,img/directories. Files inside those folders are unaffected.
Mechanics:
- 10 existing tracked files moved via
git mv(preserves blame history). - 3 new v2.1.8 files (
FGI_MemberHistory.lua,FGI_UnitTooltip.lua,FGI_ChatTooltip.lua) created directly insideModules/. - All 5 TOC files (
FastGuildInvite.toc,_BCC,_Wrath,_Cata,_Mainline) updated to reference each moved file asModules\filename.lua(Windows backslash, matches Blizzard TOC convention). - Load order preserved — the TOC reorder respects the existing dependency sequence (history before ChatMenu before ScanGroups before Core, etc.).
wow-version-replication.ps1watcher copies the new layout to_retail_/_anniversary_/_classic_trees automatically; ghost root files in the destination trees (including a straysecurity.lualeft over from v1.x in_retail_/_anniversary_) cleaned up by hand since the watcher is additive only.- Markdown link paths in
CHANGELOG.mdupdated for the 13 moved files (30 link rewrites in one pass withsed). Historical CHANGELOG entries from v1.x and early v2 contain link refs to long-removed files (mainFrame.lua,statistic.lua,antiSpamList.lua, etc.) — those were already broken before this reorg and were left alone. CLAUDE.mdupdated with a new "Repository layout (v2.1.8+)" bullet documenting the convention, plus a cleanup of the stale "Look at existing files first" bullet that mentioned ghost files (statistic.lua,mainFrame.lua,guild.lua) which haven't existed since v1.x.
Other
CLAUDE.mdrecords that the user runs BugSack + BugGrabber and doesn't need to be asked to enable Lua errors. Saves a round-trip on every future bug report.- Lint cleanup in
Modules/FGI_ChatTooltip.lua: removed unused trailing params from theOnHyperlinkEnter/OnHyperlinkLeavehandler signatures. Lua silently discards extra args from the script's caller, so the shorter signature is functionally identical and silences the language-serverunused-localhint that the leading-underscore convention didn't suppress.
[v2.1.7] (2026-05-19) — Byte-aware chat-message editors, keybind drop-out fix, docs overhaul
Live byte counter on chat-message template editors
WoW's SendChatMessage is hard-capped at 255 bytes per message, not 255 characters. The existing FGI editors enforced a SetMaxLetters(255) letter cap, which is wrong for non-ASCII users: a Cyrillic template hits the byte limit at ~127 letters (UTF-8 is 2 bytes per Cyrillic character) but the editor would silently accept input up to 255 letters, then split or get refused at send time. Korean and Chinese users are similarly affected (3 bytes per character). On top of that, FGI's three placeholder substitutions (NAME, GUILD, GUILDLINK) inflate the on-wire byte count well beyond what the user sees in the editor — GUILDLINK in particular expands to ~120 bytes for the club-finder hyperlink.
This release adds a live byte readout to the chat-message editors, with full placeholder-expansion accounting, so the number on screen is the same number SendChatMessage will see at send time.
New helper
fn:estimateSentBytes(template, mode)in functions.lua:1353. Mirrorsfn:msgMod's substitution order (NAME → GUILDLINK → GUILD) without firing its side effects (no chat prints on missing guild link). Worst-case sentinel for NAME is 24 bytes (cross-realm"Playernamename-Realmname"); GUILD reads the liveGetGuildInfo("player")result; GUILDLINK reads the cached link fromDB.factionrealm.guildLinksor falls back to a 120-byte placeholder. Literal-escapes%in cached link content before passing togsubso an unfortunate club-finder link can't break the estimator. Returns(bytes, status, chunkCount)where status is one of"ok","overflow"(single-mode, exceeds 255),"split"(split-mode,fn:messageSplitchunks it into N whispers), or"faildrop"(split-mode, single token > 255 bytes —fn:messageSplitwould silently drop the message).New custom AceGUI widget
FGI_TooltipInputBytesin GUI/SettingsPanel.lua:402. Cloned fromFGI_TooltipInputwith abyteLabelfontstring added on the labelButton row, right-anchored. Updates viaOnTextChangedon every keystroke and on programmaticSetText(so AceConfig redraws populate the initial state correctly). Grey under 80 % of the budget, amber 80–100 %, red over 100 %. Hardcoded to "single" mode (nofn:messageSplitfallback) because the editors that use it — Welcome message and Welcome whisper — go direct toSendChatMessage.SetMaxLetters(0)so non-ASCII users aren't blocked at 256 letters; the byte readout reports the true budget and the existing send-time clip is the final safety net.Wired up two single-line editors to use
FGI_TooltipInputBytes(one-linedialogControlchange each):- GUI/SettingsPanel.lua:703
welcomeMessage(Settings → Guild → Welcome on join → Welcome message) - GUI/SettingsPanel.lua:722
welcomeWhisperMessage(same panel → Welcome whisper)
- GUI/SettingsPanel.lua:703
Extended
desccallbacks on two multiline editors to include a hover-checkpoint byte readout:- GUI/SettingsPanel.lua:870
gmPolicyMessage(Settings → Guild → Guild Policy → Forced recruitment message) - GUI/SettingsPanel.lua:1438
bodyEditor(Settings → Messages → Template body)
Both use "split" mode since the content is sent via
fn:sendWhisper→fn:messageSplit. Multiline editors use AceConfig's stockMultiLineEditBoxwidget from Ace3 (not vendored by FGI) so a live in-widget counter isn't trivially achievable; thedescannotation refreshes on every hover, which is the natural editing rhythm for template-body work — type a block, hover the label to check, adjust.- GUI/SettingsPanel.lua:870
Live byte counter on the Announce tab
msgInputin GUI/Tabs/Announce.lua:284. Added amsgBytesfontstring anchoredBOTTOMRIGHTtomsgInput'sTOPRIGHT, in the same row as the "Message" label. Updates onOnTextChanged. Replaced theSetMaxLetters(255)letter cap withSetMaxLetters(0); the byte readout reports the actual budget. The "Message body" tooltip was updated to reference the byte readout instead of claiming "the input stops accepting keystrokes at 255".Locale keys added to Locale/enUS.lua (other locales fall through to the English defaults — translations welcome):
byteCountOk="%d / 255 bytes"byteCountOverflow="%d / 255 bytes — exceeds chat limit"byteCountSplit="%d bytes — sends as %d whispers"byteCountFailDrop="single word exceeds 255 bytes — message won't send"
Documentation overhaul
README.mdrepurposed as a player-facing quickstart. The previous version was a long feature list that duplicateddocs/Curseforge_Description.htmland carried several stale claims: v1 minimap behavior (left-click invite / shift-click pause / right-click main window, all wrong for v2), the removedDB.global.rememberAllsetting, a "Start Scanning" button that no longer exists,ianjplamondon-cyberas maintainer (should bePimptasty), "Message Preview" and "Custom Filter Logic" claims with no code backing, and a Sync-shares-invited-players claim that understated the actual scope (sync also covers blacklist, leave list, and tombstones). The new README is ~55 lines: a one-paragraph intro, a 6-step quickstart that maps to the actual v2 UI flow (minimap → Filters tab →>>button →+per row), a markdown table of all 11 slash commands (the old README was missing/fgi intro,/fgi dump,/fgi debug,/fgi resetWindowsPos), a pointer to in-game settings, and a Discord-only support link.docs/Curseforge_Description.htmlCommunity section now links to Discord instead of GitHub, matchingREADME.md. The "older patch notes are kept in CHANGELOG.md" line removed its GitHub link too —CHANGELOG.mdships with the addon, so users can read it directly without leaving the game..pkgmetacleanup:- Added
docs/to ignore — all dev-docs (DEV_NOTES.md,FGI_BUGS.md,Feature_Improvements.md,RETAIL_SUPPORT_DESIGN.md, the various*-plan.mdfiles, andCurseforge_Description.html) now excluded from the CurseForge zip in one rule. - Added explicit
*.ps1alongside the existing**/*.ps1for root-level redundancy in case the packager's glob semantics differ from Python's. - Removed the redundant
DEV_NOTES.mdandFGI_BUGS.mdindividual lines (now covered by the newdocs/rule).
- Added
CLAUDE.mdupdated with a new bullet documentingREADME.md's role and update triggers (only when something user-facing changes: new/removed slash commands, new keybinds, changed minimap behavior, new top-level tabs, removed features, or credits/maintainer changes — not every patch). The bullet also codifies the Discord-only-external-link rule across both player-facing docs.
Keybind drop-out bug fixed (re-targeted bindings to real visible buttons)
User report: the configured Invite next player / Start next /who scan hotkeys work for a while, then silently stop firing. The dead state survives /reload and a full client logout — only clearing and re-adding the keybind in Settings → Main → Hotkeys restores it (until the next drop). Reported on Retail; visual confirmation via repro video of spammed key presses producing no invite/scan action.
Root cause: v2.1.3 introduced a hidden-orphan-button pattern that v2.1.5's "v1-style" claim quietly preserved. init.lua created two hidden Button frames named FGI_InviteBindBtn and FGI_NextSearchBindBtn with parent = nil, and SetBindingClick targeted those names. The OnClick handlers dispatched to FGI.functions[fnName] via a runtime lookup.
The pattern diverges from v1.9.10's actual approach in two material ways. First, v1 bound the key directly to the real visible Invite / Pause-Play buttons on the main frame (interface.mainFrame.mainButtonsGRP.invite.frame:GetName() and the matching pausePlay), not to a separate hidden orphan. Second, v1's button OnClick was the same handler driving the mouse-click path, so the keybind and the mouse-click took identical code routes — there was no second hidden surface to keep alive.
Retail's binding resolver treats parent = nil orphan frames as transient — they're not part of any UI hierarchy. Mid-session (the exact trigger is observational, but the user's repro showed it consistently under spam-key conditions), the resolver drops the named frame from the active binding set. SaveBindings(GetCurrentBindingSet()) from the Settings panel set callback had already written the (now-stale) entry to bindings.wtf, so the next /reload or login restores it from disk — but the binding still resolves to a frame the resolver considers transient and silently fails to fire. Only re-running the Settings panel set callback re-pushes the binding into the active set fresh (until the next drop), which exactly matches the user's repro of "clearing and re-adding the keybind fixes it, until it stops again."
Fix in this release:
- compactFrame.lua names the two visible compact-tray buttons globally as
FGI_CompactInviteBtn(the+(N)invite button) andFGI_CompactScanBtn(the>>scan button). Previously both were anonymous (CreateFrame("Button", nil, title)). Behavior unchanged — these are the same buttons with the sameOnClickhandlers; they just have global frame names now soSetBindingClick's named-frame resolver can target them. - init.lua — the
createBindButtonhelper and the two hidden-orphan-frame creations were removed entirely (~10 lines). The replaced comment block now points readers at the compact-tray buttons. - FGI_Core.lua:820 —
OnInitialize's keybind re-application now callsSetBindingClick(key, "FGI_CompactInviteBtn")andSetBindingClick(key, "FGI_CompactScanBtn")instead of the deleted orphan names. - GUI/SettingsPanel.lua:1441, 1466 — the Hotkeys rows'
setcallbacks target the same renamed buttons.
The compact-frame buttons are created at file-parse time inside compactFrame.lua's do-blocks, so they exist as named globals the moment the addon loads. OnInitialize fires on ADDON_LOADED, which runs after every file is parsed, so by the time the keybind re-application block runs the targets are guaranteed to exist even though compactFrame.lua appears later in the TOC than FGI_Core.lua. The binding now resolves to a parented-and-anchored visible Button frame, which Retail's binding resolver retains in the active set across the full session.
Migration: users with the old orphan-frame entries in their bindings.wtf will have those stale entries silently dropped by WoW at next login (the names no longer resolve). OnInitialize then re-applies the binding to the new button names. No user action required; the in-memory binding is correct from the first login after the update. The bindings on disk get rewritten cleanly the next time the user changes a binding or WoW saves bindings on its own (e.g. closing the native Key Bindings panel).
[v2.1.6] (2026-05-19) — Compact tray bottom-half anchor flip fixed
User report: when the compact tray is positioned in the bottom half of the screen and the queue has ≤5 entries, inviting from the front pinned the bottom row in place and pushed the title bar UP one row per invite. In the top half the v2.0.8 fix worked as intended (title bar stayed put, frame shrank downward).
Root cause: WoW's built-in StartMoving / StopMovingOrSizing mover silently re-anchors a moving frame to whichever screen quadrant it ends up in. The compact tray's two OnDragStop handlers (compactFrame.lua:148 on the title bar and compactFrame.lua:219 on the counter strip) called cf:GetPoint(1) immediately after StopMovingOrSizing and persisted whatever anchor WoW chose. Dragging the tray below the center of the screen left cf:GetPoint(1) returning a bottom-anchored format (e.g. "BOTTOMLEFT" relative to "BOTTOMLEFT"). After that, cf:SetHeight() in cf.refresh() shrunk the frame relative to the bottom anchor — pinning the bottom row and pushing the title bar up.
The v2.0.8 PLAYER_LOGIN handler already had the right conversion inline at compactFrame.lua:750 — read cf:GetTop() / cf:GetLeft(), ClearAllPoints, re-anchor as TOPLEFT relative to UIParent BOTTOMLEFT in screen coords, persist. That kept the anchor invariant correct on fresh logins but didn't survive any subsequent drag.
Fix in compactFrame.lua:139: extracted that conversion into a saveTopLeftAnchor() helper, called from both OnDragStop handlers and from the PLAYER_LOGIN restore. Re-anchoring TOPLEFT to the same screen coords is idempotent, so it's safe to call from any state.
local function saveTopLeftAnchor()
if not DB then return end
local top, left = cf:GetTop(), cf:GetLeft()
if not (top and left) then return end
cf:ClearAllPoints()
cf:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
DB.global.compactFrame = DB.global.compactFrame or {}
DB.global.compactFrame.point = "TOPLEFT"
DB.global.compactFrame.relativePoint = "BOTTOMLEFT"
DB.global.compactFrame.xOfs = left
DB.global.compactFrame.yOfs = top
end
After v2.1.6, the tray's anchor invariant is enforced after every drag-stop AND on every login, so cf:SetHeight() always shrinks the frame downward regardless of which screen quadrant the user dropped the tray in.
[v2.1.5] (2026-05-19) — GM policy, whisper-invite delay, multi-select queue, v1-style keybinds
Four user-requested features + a real fix for the Retail keybind warnings the v2.1.3 attempt introduced.
Feature 2 — GM Override / Force Settings (Guild Policy)
The Guild Master can now broadcast a forced recruitment message body and a minimum anti-spam retention to every FGI user in the guild. Officers can lengthen retention beyond the floor but cannot drop below it, and they can't edit the message body or pick a different template while a forced message is in effect.
- DB default at FGI_Core.lua:528:
DB.factionrealm.gmPolicy = nil. Active policy is a table{ active, message, antiSpamMin, setBy }. - Message override at functions.lua:1462:
fn:getRndMsg()short-circuits topolicy.messagewhengmPolicy.active and gmPolicy.message ~= "". The localmessageListis untouched -- when the policy clears, recruiters fall back to their own templates instantly. - Sync via the existing
FGISYNC_PREFIX_Gaddon channel. Push button at GUI/SettingsPanel.lua:937 encodes aGMPOLICYmessage and broadcasts viaChatThrottleLib:SendAddonMessage("NORMAL", FGISYNC_PREFIX_G, data, "GUILD"). Receive handler at functions.lua:3265 stores the incoming policy intoDB.factionrealm.gmPolicyand prints a notification line (gated onnot addonMSG and not muteSync-- the polarity matches every other print site in the file; the Copilot draft had this inverted and I fixed it before commit). - GM detection via
select(3, GetGuildInfo("player")) == 0(rank 0 = Guild Master). The Guild Policy widgets in the Guild tab aredisabled = function() ... end(visible-but-locked) for non-GMs rather than hidden, so everyone in the guild can see the active policy and who set it. - clearDBtimes floor at GUI/SettingsPanel.lua:1052:
getreturnsmax(stored_v, gmPolicy.antiSpamMin)so the dropdown visually reflects the floor even when the user's stored value is shorter. The stored value isn't mutated -- if the policy is cleared the user reverts to their original choice.v = 1("Never expire") is the most restrictive option and is never bumped. - Message-body lock at GUI/SettingsPanel.lua:1418:
currentMessageandbodyEditorboth extend theirdisabledpredicate togmPolicy.active and gmPolicy.message ~= nil.bodyEditor.descbecomes a function appending an orange "Locked by GM policy (set by ...)" note when active.
Feature 4 — Whisper-to-invite delay
- New
DB.global.whisperDelay = 0default at FGI_Core.lua:613. - Mode 2 (
Whisper + invite) path at functions.lua:1487 now wraps the post-whisper invite block inC_Timer.After(delay, doInvite)whenwhisperDelay > 0. Whendelay == 0the inner function runs synchronously (unchanged from prior behaviour). - Slider in GUI/SettingsPanel.lua:1373: range 0-10 seconds, step 0.5, under the Messages section as order 2 (right under the
messagesHeadertooltip). A short delay (1-2s) lets the candidate read the whisper before the invite dialog appears.
Feature 5 — Multi-select scan queue with batch invite
addon.search.selected = {}table appended to theaddon.searchdefinition at functions.lua:20 and cleared infn.clearSearchat functions.lua:2562. Keyed by player name (same key the RowList row uses).- New checkbox column prepended to the queue RowList at GUI/Tabs/Scan.lua:709.
key="selected",width=20,boxSize=14,onTogglewrites toaddon.search.selected[entry.name]. - Two new strip buttons:
Sel All(48 px) -- toggles between "all selected" and "none selected" based on current count. Anchored left of the Mode dropdown.+(N)sel(64 px) -- batch-invites every checked queue entry viafn:invitePlayer(false, idx), iterating highest index first so removals don't shift earlier indices. Clearsaddon.search.selectedand refreshes after.
ScanTab.MIN_WIDTHbumped from 720 to 840 to fit the new buttons + the existing strip widgets.ScanTab.Refresh()updates the+(N)sellabel live as boxes are checked/unchecked.
Feature 1 — %GUILD% and %GUILDLINK% placeholders surfaced in tooltip
Doc-only change at GUI/SettingsPanel.lua:1352. messagesHeader.desc now mentions %GUILD% (replaced with the guild name in <> brackets) and %GUILDLINK% (Retail-only clickable guild recruitment link) alongside the existing %player% and date-format placeholders. The substitution logic in fn:msgMod was already in place from earlier releases -- this just stops users from discovering the placeholders by accident.
Keybinds: real fix replacing the v2.1.3 Bindings.xml approach
v2.1.3 tried to use Bindings.xml + BINDING_HEADER_* / BINDING_NAME_* globals so FGI keybinds would show up in ESC > Options > Key Bindings > AddOns > FastGuildInvite. On Retail (11.x) Blizzard removed <Binding> from the XML schema entirely, so the declared actions became silent no-ops and the file emitted Unrecognized XML: Binding warnings on every /reload. The Copilot draft attempted to suppress the warning by removing Bindings.xml from _Mainline.toc but kept the file on disk -- which is the wrong fix because (a) WoW auto-loads Bindings.xml from the addon root regardless of TOC reference, and (b) the bindings still didn't actually work on Retail.
v2.1.5 reverts to v1.9.10's pattern verbatim: runtime SetBindingClick(key, frameName) against hidden named Button frames. Works on every WoW version (Classic, TBC, Wrath, Cata, Retail) because SetBindingClick is a runtime API call, not an XML declaration.
- Hidden Button frames at init.lua:21-49:
FGI_InviteBindBtnandFGI_NextSearchBindBtn. Each has anOnClickthat looks upFGI.functions:invitePlayer()/:nextSearch()at call time (not bind time) becausefunctions.luahasn't loaded yet wheninit.luaruns. By the time the user actually presses the bound key, the methods exist. DB.global.keyBindat FGI_Core.lua:615:{ invite = nil, nextSearch = nil }defaults. Persists the user's chosen keys across/reload.- Hotkeys settings rows at GUI/SettingsPanel.lua:1024: same two
type="keybinding"rows from v2.1.3, but thegetreadsDB.global.keyBind.{invite,nextSearch}and thesetclears the previous key (SetBinding(prev)), writes the new one (SetBindingClick(key, "FGI_InviteBindBtn")), and persists viaSaveBindings(GetCurrentBindingSet()). The descriptive blurb updated to mention the bindings live only in this panel (not in WoW's native Key Bindings) -- matches v1 behaviour. - OnInitialize re-apply at FGI_Core.lua:806: replays
SetBindingClickfromDB.global.keyBindon every login.SetBindingClickis transient (doesn't survive/reloadby itself) so the replay is necessary. Bindings.xmlat addon root is now an empty stub file containing only a comment explaining why -- WoW auto-loads anyBindings.xmlfrom an addon root, and an absent file generatesCouldn't openwarnings. The stub satisfies the auto-load without declaring any<Binding>elements (which would error on Retail).- TOC references removed from all 5
.tocfiles (FastGuildInvite.toc,_BCC.toc,_Cata.toc,_Mainline.toc,_Wrath.toc). BINDING_HEADER_FASTGUILDINVITEandBINDING_NAME_FGI_*globals removed from init.lua. They're meaningless without an XML declaration.
Trade-off (same as v1): FGI keybinds don't appear in WoW's native ESC > Options > Key Bindings panel. Users configure them only inside the addon's Hotkeys settings. v1 shipped this way for years and nobody complained -- recruiters go to the addon's UI to bind FGI hotkeys, not WoW's panel.
[v2.1.4] (2026-05-19) — System-message mute sledgehammer restored
Third v1 toggle restoration in two days. systemMSG was the v1.9.10 toggle ("Выключить системные сообщения" / "Turn off system messages") that registered a CHAT_MSG_SYSTEM filter unconditionally returning true -- dropping every system message that would otherwise reach the chat frame. The DB.realm.systemMSG flag survived the v2 strip and the SettingsPanel toggle existed, but the filter registration (v1's updateMsgFilters() calling ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", fn.hideSysMsg)) was lost when settings.lua was deleted. The toggle was inert.
Restored wiring
fn.hideSysMsgat functions.lua:2846 was already intact from v1 -- a one-line callback that returnstrueto drop every CHAT_MSG_SYSTEM event. No edit needed.fn.updateSystemMsgFilterat functions.lua:1399 -- NEW. Mirrorsfn.updateWhisperEchoFilter(today's v2.1.3 work): idempotentChatFrame_AddMessageEventFilter/ChatFrame_RemoveMessageEventFilteragainstCHAT_MSG_SYSTEMkeyed onDB.realm.systemMSG. Safe to call from any state.- OnInitialize call at FGI_Core.lua:797 -- runs
fn.updateSystemMsgFilter()once after DB wiring alongsidefn.updateWhisperEchoFilter(), so the persisted toggle state survives login without/reload. - SettingsPanel toggle at GUI/SettingsPanel.lua:1014 -- renamed from the misleading
"Show invite system messages in chat"(inverted polarity vs. v1) to the honest red-labelled"Mute ALL WoW system messages (sledgehammer)". Description is a multi-line WARNING block listing each category of system message that goes silent (server errors, online/offline notices, manual /who results, etc.) so users don't toggle blindly.setcallback now callsfn.updateSystemMsgFilter()for live apply.
Polarity matches v1 semantics: systemMSG = true registers the filter (mute). The v2 toggle's previous label suggested the opposite reading; no migration needed since the flag has been a no-op in v2 so any persisted value carries no behavioural meaning users have observed.
Classic / TBC / Wrath / Cata path is identical to retail. No gv.isRetail gates -- chat-event filters work the same on every client.
[v2.1.3] (2026-05-19) — Outgoing-whisper echo suppression restored + keybinds back
Two user-requested restorations of v1 features the v2 strip lost.
Outgoing-whisper echo suppression
v1's sendMSG toggle hid the CHAT_MSG_WHISPER_INFORM echo line ("To [Player]: ...") for whispers the addon itself sent during bulk invites. The flag persisted into v2 but was relabeled to a misleading "Show /who results in chat" (the v2 strip dropped settings.lua's updateMsgFilters() wiring and the new SettingsPanel toggle was reskinned by someone who didn't trace the flag back to its actual behaviour). The toggle did nothing in v2.
Restored end-to-end:
fn.hideWhisperat functions.lua:1353 — the chat filter callback was never deleted in the strip; survived intact. Matches outgoing whisper text againstaddon.removeMsgList[fullPlayerName(name)]and drops the matching CHAT_MSG_WHISPER_INFORM event. Multi-part whispers (fn:messageSplit) match-and-remove each segment individually so the keyed table can't grow unbounded.fn:sendWhisperat functions.lua:1397 — already conditionally populatedremoveMsgList[key]whenDB.realm.sendMSGwas true. Also intact from v1; no edit needed.fn.updateWhisperEchoFilterat functions.lua:1378 — NEW. IdempotentlyChatFrame_AddMessageEventFilter/ChatFrame_RemoveMessageEventFilteragainstCHAT_MSG_WHISPER_INFORMbased on currentDB.realm.sendMSG. Replaces v1'supdateMsgFilters()(which the v2 strip deleted along withsettings.lua).- OnInitialize call at FGI_Core.lua:796 —
fn.updateWhisperEchoFilter()runs once after DB wiring so the persisted toggle state is applied at login without /reload. - SettingsPanel toggle at GUI/SettingsPanel.lua:1000 — renamed from the misleading
"Show /who results in chat"to"Hide outgoing whisper echoes"with an honest description. ThechatHeadersection renamed from"/who chat output"to"Chat noise".setcallback now callsfn.updateWhisperEchoFilter()after writingDB.realm.sendMSGso flipping the toggle takes effect live.
Classic / TBC / Wrath / Cata path is identical to retail — fn.hideWhisper doesn't gate on gv.isRetail and the filter registration goes through the same ChatFrame_AddMessageEventFilter API on every client.
Native keybinds restored
v1 had two keybinds (Invite, Next-search) configured inside the legacy KeyBind tab in the old settings popup; both went with the popup in v2.1.0. v2 plan §Implementation order step 10 specifies the replacement: WoW-native Bindings.xml + BINDING_HEADER_* / BINDING_NAME_* globals so bindings live under ESC → Options → Key Bindings → AddOns → FastGuildInvite rather than buried in a custom in-addon panel.
- Bindings.xml at addon root, listed at the bottom of every TOC (
FastGuildInvite.toc,FastGuildInvite_BCC.toc,_Cata.toc,_Mainline.toc,_Wrath.toc). Two<Binding>elements:FGI_INVITE→FGI.functions:invitePlayer()(head-of-queue invite)FGI_NEXTSEARCH→FGI.functions:nextSearch()(next /who tick)- Both
category="ADDONS"andheader="FASTGUILDINVITE".runOnUp="false"so key-up doesn't re-fire.
- Binding labels in init.lua:23-27 —
BINDING_HEADER_FASTGUILDINVITE = "FastGuildInvite",BINDING_NAME_FGI_INVITE = "Invite next player",BINDING_NAME_FGI_NEXTSEARCH = "Start next /who scan". Defined early so the binding panel has the strings whenever the user opens it. - In-addon convenience at GUI/SettingsPanel.lua:1024 — new
Hotkeyssection on the Main page with two AceConfigtype = "keybinding"rows.getreturnsGetBindingKey(action),setclears the prior key (SetBinding(prev)), applies the new key (SetBinding(key, action)), and persists viaSaveBindings(GetCurrentBindingSet()). Both surfaces edit the same underlying binding — setting a key in one place updates the other. Account-wide by default (saves toACCOUNT_BINDINGSunless the user has toggled WoW's binding panel to character-specific).
[v2.1.2] (2026-05-18) — Scan status-bar gated to Scan tab
User-reported polish: kicking off a scan and switching to (say) Blacklist still painted the orange progress fill on the status bar and showed Scanning N% | X / N queries in the status text. The status bar is part of the AceGUI Frame's chrome — visible on every tab — so the scan-progress paint bled into tabs where it wasn't relevant. v2.1.1 gates the scan-progress content to self.activeTab == "scan" while leaving the status bar (and its version-string default) intact on every tab. Scanning itself is untouched.
New MainWindow:RefreshStatusBar()
Added in GUI/MainWindow.lua between SetScanProgress and Open. Single source of truth for status-bar paint:
- Reads
addon.search.progressTotal/progressDoneandself.activeTab. - If
activeTab == "scan"ANDtotal > 0ANDdone < total→ paintsversion | Scanning N% | X / N queriestext and callsSetScanProgress(done, total)to show the orange fill. - Otherwise → resets text to bare
addon.versionand callsSetScanProgress(0, 0)to hide the texture.
Scan tick consolidated to one call
GUI/Tabs/Scan.lua's ScanTab.Refresh() previously had a dual-fire block: built status text via a local buildStatusText() and called both SetStatusText + SetScanProgress itself. Replaced with a single addon.MainWindow:RefreshStatusBar() call so all gating logic lives in one place. Removed the now-unused buildStatusText() helper (~25 lines).
Tab-switch refresh
OnGroupSelected in GUI/MainWindow.lua now calls self:RefreshStatusBar() after swapping activeTab, so switching from Scan → Blacklist mid-scan immediately clears the orange fill and percentage text, and switching back to Scan re-paints them on the next tick.
LICENSE file added
New top-level LICENSE file at the addon root. Same "All Rights Reserved" structure as the Recount addon's LICENSE: copyright lines naming the original author (Knoot0279, through 2025-11-22 — the day this repo's cb89eca "Initial Commit." landed) and the current maintainer (Pimptasty, from that date forward), followed by the standard reservation-of-rights paragraph and a closing paragraph noting this is a maintained fork. Not listed in .pkgmeta's ignore: block so the BigWigs packager picks it up automatically and ships it in the CurseForge zip. No code changes — purely a credit/copyright addition.
Welcome-spam on retail — third-pass fix in LibGuildRoster-1.0
v2.0.8 vendored the lib with a one-shot wasInitialized flag; v2.1.0 added STABLE_THRESHOLD = 2 (two consecutive same-total GUILD_ROSTER_UPDATE events before flipping initialized). Tested green on classic, regressed on retail: in a guild of ~200, a /reload produced ~20 welcomes in [Guild] plus a flood of "No player named 'X' is currently playing." whisper attempts and "The number of messages that can be sent is limited..." throttle errors.
Root cause: on retail the server can send several GUILD_ROSTER_UPDATE events at the same partial total before the full roster arrives. STABLE_THRESHOLD = 2 then "stabilizes" on a partial snapshot, and the next event with the real roster diffs the remaining members as fresh joins. The threshold heuristic is unfixably racy here — there's no count-based signal that says "the stream is definitely complete now."
Fix in Libs/LibGuildRoster-1.0/LibGuildRoster-1.0.lua:
OnMemberJoinedno longer fires from the post-rebuild diff. The diff still drivesOnMemberOnline(existing-member-comes-online), which has never been the misfire source.OnMemberJoinednow fires exclusively fromOnChatMsgSystem's"X has joined the guild"branch. The chat-system message is the authoritative server signal — it only fires when a real join happens, never during login roster population.NormalizeNamealready handles both Classic ("Player") and retail cross-realm ("Player-Realm") forms.RequestGuildRoster()is still triggered from the chat-system handler soIsInGuild/GetMember/GetAllMembersconsumers reflect the new member, but the join callback no longer waits for that round-trip.- STABLE_THRESHOLD = 2 stays, now solely gating
OnRosterReadyand the OnMemberOnline diff (so existing-member-comes-online events don't fire during the login stream). MAJOR, MINORbumped from1to2so LibStub picks this copy over any pristine vendored copy in another addon.
Trade-off: a join that happens while the user is offline does not fire OnMemberJoined on next login (the chat-system message was sent before PLAYER_LOGIN, so this client never received it). Acceptable — the alternative is the welcome-everyone bug we're fixing.
Marked with FGI vendor fix inline alongside the existing markers.
[v2.1.0] (2026-05-18) — v2 UI Overhaul Complete: Legacy Strip + Welcome-Spam Fix + Note-API Taint Guard
The final phase of the v2 UI overhaul (see docs/v2.0-plan.md §Implementation order Phase 11 + docs/phase11-plan.md). Removes every legacy popup-soup file now that the modern UI handles all surfaces, recovers logic the v2 strip left stranded, and ships two retail/Anniversary fixes.
Removed: 15 legacy UI files (~5000 lines)
Deleted in one strip pass, with their TOC entries removed from all 5 .toc files:
mainFrame.lua,settings.lua,guild.lua,credits.lua,keybindings.lua(the 5 root files of the legacy main UI)antiSpamList.lua,blackList.lua,customList.lua,filtersFrame.lua,quietList.lua,inviteHistory.lua,statistic.lua(legacy sub-panels migrated to v2 tabs)logs.lua,message.lua,searchByLocation.lua(single-toggle / data-only files folded into AceConfig or absorbed into the scan engine)synch.lua(was already entirely inside a--[[ ... ]]block comment; deleted as dead text)
Net diff for the strip commit alone: 37 files changed, +561 / -5874 lines.
Entry points retargeted to v2 paths
- Minimap LMB →
addon.MainWindow:Toggle()(orinterface.compactFrame:Show/HideperpickOpenView()). - Minimap RMB →
addon.SettingsPanel:Open()(Blizzard's ESC > Options > FastGuildInvite viaSettings.OpenToCategoryon retail /InterfaceOptionsFrame_OpenToCategoryon classic). /fgi show→ routes throughpickOpenView()for compact-vs-main, thenaddon.MainWindow:Open(). Thefn.showAddonindirection is inlined and deleted./fgi nextSearch→fn:nextSearch()directly (was clickinginterface.mainFrame.mainButtonsGRP.pausePlay.frame)./fgi resetWindowsPos→ resets only the surviving frames (interface.dumpWindow,interface.debugFrame,interface.compactFrame) plusDB.char.frames.mainWindow = nilso the next:Open()re-centers./fgi v2removed entirely (was the dev alias for the v2 preview during phase rollout; redundant now that v2 is the only UI).
Recovered: logic the strip left stranded
The legacy files held more than just UI. Three concrete cases of live code paths that lost their source-of-truth and were recovered:
fn.historyaggregator lived instatistic.lua. After deletion, scan/invite events calledfn.history:onSearch()/:onSend()/:onAccept()/:onDecline()/:onDeclineAuto()/:onFound()/:onLeave()/:joined()/:logInvite()/:trim()against nil, crashing on the next scan withattempt to index field 'history' (a nil value)at functions.lua:2746. Recovered as a new kept logic file history.lua at addon root, registered in all 5 TOCs afterfunctions.lua. The legacy graph-UI refresh side (refreshTotals,drawGraph,refreshHistoryPage) was dropped — the Statistics and History tabs refresh themselves via their own OnShow paths. Every methodaddon.DB-nil-guarded.addon.syncUIstate holder + four sync callbacks (fn.onSyncStarted,fn.onSyncSuccess,fn.onSyncFailed,fn.onSyncNobody) lived insettings.lua's legacy Sync sub-page. GUI/SettingsPanel.lua:1129's runSync button readsaddon.syncUI.{inProgress, resultText, manualClick, setResult}to render its label / disabled state. Appended to functions.lua (sync callbacks live nearfn.startSyncwhich is also in functions.lua). The legacy AceGUI button-refresh path is dropped; only theAceConfigRegistry-3.0:NotifyChange("FastGuildInvite")path remains.fn.showAddonlived inmainFrame.luaas a one-call-site router used by/fgi show. Inlined into FGI_Core.lua's slash-command dispatch rather than recreated.
Locale orphan sweep
Across all 7 locale files (enUS, ruRU, zhCN, zhTW, frFR, deDE, koKR): 165 unused L["key"] entries removed. Generated by diffing the deleted-file L["..."] references against current live-code references, then per-key-confirming that ASCII keys also weren't reached via L.key accessor syntax.
Caught + corrected during the sweep: Locale/summary.lua reads L["Автор"] / L["Имя"] / L["Перевод"] / L["Тестирование"] / L["Черный список"] for the credits table. The initial sweep dropped them as orphan because the live-key grep excluded all of Locale/. Restored across all 7 locales with their original translations. Memory [[project-locale-summary-reads-keys]] notes the gotcha for future passes.
Tooltip consistency pass
11 raw GameTooltip:SetOwner(...) call sites in v2 GUI code (compactFrame.lua, GUI/MainWindow.lua, GUI/RowList.lua, GUI/UI.lua, GUI/Tabs/{Announce,Filters,Scan}.lua) all sat in dead else fallback branches: if addon.Tooltip and addon.Tooltip.Owner then addon.Tooltip.Owner(self_) else GameTooltip:SetOwner(self_, "ANCHOR_*") end. The else branches never fire because addon.Tooltip.Owner is defined in functions.lua:452 which loads before every v2 GUI file. Simplified each site to a direct addon.Tooltip.Owner(...) call — every tooltip in the main UI now goes through the auto-flipping helper. Net -45 lines, no behavioural change.
Orphan widget classes deleted from Libs/GUI.lua
Five custom AceGUI widget classes that were only ever instantiated by the deleted legacy files: FilterButton (fn:FiltersInit), TCheckBox (legacy filters/settings panels), TKeybinding (keybindings.lua), ProgressBar (mainFrame.lua), TButton (legacy settings popup widgets). Removed in line-range deletions, file went from 1659 → 596 lines (-1063). Surviving custom widgets: ClearFrame (dump/debug), GroupFrame (dump), TLabel (intro).
Welcome-message spam on retail — second-pass fix in LibGuildRoster-1.0
v2.0.8 vendored the LibGuildRoster-1.0 library to centralize join/leave detection, with a wasInitialized guard intended to suppress welcomes during the initial roster build. The guard required only ONE successful GUILD_ROSTER_UPDATE to flip — but on retail, GUILD_ROSTER_UPDATE fires multiple times after PLAYER_LOGIN and any RequestGuildRoster() call as the roster streams in across events. Event 1 captures a partial roster (e.g. 50 currently-online members), sets initialized = true. Event 2 brings the full guild (e.g. 200 members). The diff between event 2's full roster and event 1's partial roster treats the 150 newly-visible members as fresh joins, fires OnMemberJoined for each, and our welcome handler sends a welcome message + whisper for every one of them.
Fix in Libs/LibGuildRoster-1.0/LibGuildRoster-1.0.lua:
- Added
lib.stableCount/lib.previousTotal/lib.STABLE_THRESHOLD = 2. - In
OnGuildRosterUpdate, whilenot self.initialized, incrementstableCountwhen the new total matches the previous total (and > 0); reset to 0 otherwise. Only setinitialized = trueand fireOnRosterReadyoncestableCount >= STABLE_THRESHOLD. - During the stabilization phase each rebuild still updates
self.rostersilently — no transition callbacks fire. - After stabilization, normal diff logic resumes; real mid-session joins fire
OnMemberJoinedimmediately (the guard skips the already-stable path).
Marked with the FGI vendor fix inline comment alongside the existing vendor fixes (RequestGuildRoster wrapper, recentlyLeft dedup, nil-branch for the documented OnMemberJoined callback) so a future re-vendor diff is obvious.
ADDON_ACTION_FORBIDDEN: SetNote() taint guard
User report on TBC / Anniversary: opening the in-game guild panel and editing a member's note via the right-click "Edit Note" path raised ADDON_ACTION_FORBIDDEN AddOn 'FastGuildInvite' tried to call the protected function 'SetNote()'. Stack trace pointed at Blizzard_StaticPopup_Game/GameDialogDefs.lua:1855 (the Blizzard popup's EditBoxOnEnterPressed), not at FGI code — Blizzard attributed the protected call to FGI because of taint propagation.
Root cause: on every Classic-family client that exposes C_GuildInfo.SetNote (TBC 2.x patches, Wrath, Cata, Anniversary, and Retail), the legacy GuildRosterSetPublicNote / GuildRosterSetOfficerNote entry points have been backed by the protected C_GuildInfo.SetNote. They look like the old API but internally call the protected modern one. Our pcall(GuildRosterSetPublicNote, ...) raises the forbidden-action event and leaves taint that propagates to the user's later in-game guild-panel actions; pcall suppresses the error message but doesn't grant secure context.
Fix in two places:
- FGI_APICompat.lua — added
noteAPIsRestricted()probe (C_GuildInfo and C_GuildInfo.SetNote and true or false).API.SetPublicNoteandAPI.SetOfficerNoteshort-circuit at the probe before touching the legacy entry points when it returns true. - functions.lua:738 (
fn:setNote) — early-return at the top whenC_GuildInfo.SetNoteexists, so the roster walk +CanEditPublicNotechecks don't fire either. The join-time trigger at functions.lua:589 also skips theaddon.API.GuildRoster()refresh + 5s timer when notes are restricted.
Behaviour after the fix: Real Classic Era 1.15.x (no C_GuildInfo.SetNote) still supports auto-note. TBC / Wrath / Cata / Anniversary silently skip — no taint propagation, no spurious blame on the user's manual guild-panel edits. Retail unchanged (already guarded by gv.isRetail).
Other cleanups
/fgi v2slash command removed along with its help-text line./fgi showis the single entry point now.- Dead constants in FGI_Constants.lua: removed
FGI_DEFAULT_SEARCHINTERVAL,FGI_SEARCHINTERVAL_MAX,FGI_FILTERSLIMIT,FGI_MAXWHOQUERY,FGI_BLACKLIST_MAX(defined, no readers). DB.global.scanFrameChildsSV default removed from FGI_Core.lua (legacy mainFrame child-visibility toggles; no live readers).addon._pickOpenViewpublic exposure removed (was for the inlined-then-deletedfn.showAddon; the innerpickOpenViewlocal is still used).- Title bar strips the
v2.0 (preview)qualifier — now reads"FastGuildInvite". - Historical comment prune throughout the codebase:
Phase N,v2.0.X:,v1.x,legacy popup,legacy main UI,until Phase 8 retiresreferences removed or reworded to describe current behaviour. Three documentation-style files (init.lua:41's git-tag stripping example,intro.lua'sCURRENT_UPDATESuser-facing release notes, andSettingsPanel.lua:1289's Pimptasty contributor bio) keep their version references intentionally — they're either current behaviour or historical fact. DB.global.keyBinddefault removed (the Invite / Next-search keybind feature was deleted; see entry below).
Removed: Invite and Next-search keybinds
The two keybinds the addon previously offered were configured inside a dedicated KeyBind tab inside the legacy settings popup. With that popup retired, the keybinds went with it — both actions are one click away on the visible +(N) invite button and >> scan button on every view. Net code removed: keybindings.lua (54 lines), fn:SetKeybind (19 lines in functions.lua), bootstrap calls in FGI_Core.lua, DB.global.keyBind default, and the use keybinds analytic. If a user had keybinds saved in their SavedVariables, the orphan keys are silently ignored (AceDB doesn't error on unknown keys).
TOC files
No ## Version: edits — the BigWigs packager substitutes FastGuildInvite-v2.1.12 from the git tag at release-build time.
[v2.0.9] (2026-05-16) — Phase 9: Announce Feature (Profile-Based, v2 Main-Window Tab)
Announce feature shipped per docs/v2.0-plan.md §Announce, redesigned twice during development before landing on the final shape:
- Plan's design — per-channel configuration with one message per channel, settings in an AceConfig sub-page.
- First iteration — profile-based model (N named profiles, each with multi-channel selection), still in an AceConfig sub-page rendering each profile as an inline group.
- Final — same profile-based model, but moved to a dedicated Announce tab on the v2 main window (alongside Filters / Blacklist / Anti-Spam / Custom Scan / etc.) using the addon's standard RowList component. The AceConfig sub-page is now a small pointer that opens the new tab.
The move to a v2 tab came from a string of user feedback that the AceConfig version couldn't deliver: multi-select dropdown for channels (AceConfig can only render inline checkboxes for multiselect), compact horizontal form strip on 2-3 lines (AceConfig's flow layout adds widget padding), columned list view of saved profiles with sortable headers and per-row Active checkboxes (AceConfig has no RowList equivalent). The v2-tab approach inherits all of that for free from the existing pattern Filters / Blacklist / etc. use.
New file: Announce.lua
- Module shape mirrors
Scan.lua/synch.lua— non-UI logic file at addon root, registered in all 5 TOCs (FastGuildInvite.toc,_BCC,_Cata,_Mainline,_Wrath) right aftersynch.lua. Module table exported asaddon.announce(lowercase, matching the conventionaddon.search,addon.searchInfo). - Profile lifecycle —
:GetProfiles(),:GetProfile(idx),:CreateProfile(name),:DeleteProfile(idx),:DuplicateProfile(idx). Each profile is{ id, name, enabled, message, cooldown, activity, channels = { [key] = true } }. Theidis a stable time-prefixed string generated at creation (time() .. "_" .. random(1, 999999)); state is keyed off it so profile renames don't break per-(profile, channel) cooldown tracking and deletes can clean up just that profile's state entries. - Eligibility gate (
Announce:IsEligible(profile, channelKey, now)) — two-layer check per (profile, channel):now - lastPosted >= FGI_ANNOUNCE_MIN_COOLDOWN— hardcoded 60-second floor enforced regardless of the profile'scooldownsetting. Defence in depth: a corrupted SV or sync payload cannot drop below this.now - lastPosted >= profile.cooldownOR(profile.activity > 0 AND msgsSinceLastPost >= profile.activity)— normal cooldown elapsed, OR the channel's activity threshold has been exceeded (your line has scrolled off-screen). Activity bypass is opt-in per profile (defaultactivity = 0disables it).
time()notGetTime()forlastPosted—GetTime()resets to ~0 on every/reload, which would let users bypass cooldowns by reloading; using Unix epoch keeps the stored value meaningful across reload boundaries.Announce:Send(opts)— iterates every Active profile, then iterates each profile's selected channels. For every (profile, channel) that passes the eligibility gate, callsSendChatMessage(profile.message, "CHANNEL"|"GUILD"|"OFFICER", nil, idx?)and updatesstate[profile.id .. ":" .. channelKey]. Skips silently when:- Master toggle is off (
DB.factionrealm.announce.enabled == false). ChatThrottleLib.Frame.size > 50— Grouper-style queue-health guard, nil-checked so the lib's absence degrades to directSendChatMessagewith no guard.- The channel is no longer joined (
GetChannelName(name)returns nil) — silent no-op rather than logged error. - Officer chat is selected but the player isn't an officer (
CanEditOfficerNote()returns false) — proxy check for "can post to officer chat" in classic-era. - Two profiles can target the same channel and both fire in a single click if both pass their own per-(profile, channel) gate. Acceptable v2.0 behaviour — the user clicks the horn manually rather than the addon firing on a timer, so the cooldown's real purpose is rage-click suppression rather than preventing auto-spam.
- Master toggle is off (
- Activity-counter event handlers — module-scoped frame registers
CHAT_MSG_CHANNEL,CHAT_MSG_GUILD,CHAT_MSG_OFFICER. Channel handler reads arg9 (channelBaseName) from the documented event payload. Counter is bumped for every Active profile that has the channel selected — so a chat tick onLookingForGroupbumps the activity counter for every profile that posts there. Announce:GetStatus()— returns an array of{ profileName, eligible, remaining, channelCount }for the horn-icon tooltip on the v2 main window and the compact tray. Per-profile aggregate:eligible = trueif ANY of the profile's selected channels can fire now;remainingis the minimum seconds until any channel becomes eligible.Announce:HasActiveProfiles()— used by the AceConfig "Send now" button'sdisabledcallback and the horn-icon tooltip to decide whether clicking will do anything. An "active" profile must be Active + have at least one channel selected + have a non-empty message body.
New constant: FGI_ANNOUNCE_MIN_COOLDOWN = 60
Added to FGI_Constants.lua. Referenced in two places:
- AceConfig cooldown slider's
minvalue in GUI/SettingsPanel.lua (client-side enforcement). - Runtime eligibility gate in
Announce:IsEligible()(defence-in-depth — fires even if a value bypassed the slider via sync or manual SV edit).
New DB defaults
In FGI_Core.lua:
DB.factionrealm.announce = { enabled = false, profiles = {} }— configuration shared across same-faction-realm characters. The profiles array is empty until the user clicks "+ New profile" on the settings sub-page. No migration code needed — Phase 9 ships fresh in v2.0.9, so there's no released data to migrate.DB.char.announce.state = {}— per-character runtime state, lazily populated withstate["<profileId>:<channelKey>"] = { lastPosted = 0, msgsSinceLastPost = 0 }on first touch. Per-character scope is deliberate: an alt swap should not import another character's cooldowns (the plan flagged this as acceptable v2.0 behaviour).DeleteProfilewalks state and removes orphanedprofileId:*entries so the table doesn't accumulate dead keys across many delete/recreate cycles.
New v2 main-window tab: Announce
New file GUI/Tabs/Announce.lua, registered in all 5 TOCs after GUI/Tabs/CustomScan.lua. Registered as the 9th tab in GUI/MainWindow.lua's TAB_DEFS (after Quiet Zones), with help-tooltip content in TAB_HELP.announce and a TAB_MODULE.announce = "AnnounceTab" mapping for the resize-floor logic. MIN_WIDTH = 720, MIN_HEIGHT = 400 — same floor as Filters.
Modeled directly on GUI/Tabs/Filters.lua — same strip-on-top + RowList-below pattern, same idioms (placeLabelAbove, attachTooltip, Save-button-greyed-on-empty-name from v2.0.8, click-row-to-load-into-form). The multi-select dropdown for channels is the same UIDropDownMenuTemplate + info.keepShownOnClick checkable-items pattern Filters uses for Classes / Races.
Top form strip (78 px tall, 2 rows):
- Row 1:
[Profile Name](140 px)[Channels ▾](180 px, multi-select dropdown)[CD](50 px numeric)[Act](50 px numeric)[Save](64 px). Save is greyed until the Name field is non-empty (mirrors Filters' v2.0.8 behaviour). - Row 2:
[Message](flex-width, single-line, capped at 255 characters bySetMaxLettersat the source — WoW'sSendChatMessagelimit)[Send](60 px). Send firesaddon.announce:Send(). - All inputs use
InputBoxTemplate; the dropdown usesUIDropDownMenuTemplatewith the same -8 anchor offset Filters uses to compensate for the chevron's internal padding. - Labels above each widget via
addon.UI.MakeLabelso the header text gets the same tooltip-on-hover pattern Filters uses (clickable label = section help).
RowList columns (saved profiles list below the strip):
| Column | Width | Notes |
|---|---|---|
| On | 36 | Checkbox column. onToggle callback sets profile.enabled directly so users can pause/unpause a profile without editing it (matches Filters' Active checkbox). |
| Name | 140 | Click the row to load the profile into the strip for editing. |
| Channels | auto | buildChannelSummary() formats as "Guild, Officer, LFG (+1)" — up to 3 names then +N. |
| CD | 50 | Right-justified. Cooldown in seconds. |
| Status | 100 | Right-justified. Live readiness pill: Ready (≥1 channel eligible) / on cooldown / (paused) / (no chans). |
Plus an X delete-action icon on the right of each row (Blizzard UI-GroupLoot-Pass-Up texture). Header tooltips on every column (headerTip field on the column spec).
Save semantics mirror Filters' doSave: if the row was loaded via click (editingId set), Save updates that specific profile (so renames via Name field actually rename in place rather than spawning a duplicate). If editingId is unset (fresh form), Save creates a new profile via addon.announce:CreateProfile(name). New profiles default enabled = true; existing profiles keep their current enabled state across Save (the per-row checkbox is the canonical toggle).
Numeric input validation — Cooldown is clamped to [FGI_ANNOUNCE_MIN_COOLDOWN, 3600] on Save; Activity to [0, 50]. Both are floored to integers. Out-of-range or non-numeric text falls back to the floor / 0 respectively.
AceConfig sub-page deleted entirely
GUI/SettingsPanel.lua's announce group is gone. The brief pointer-page that replaced the inline-group implementation was itself removed once the v2 tab proved sufficient — the horn icon on the v2 main window's bottom row and the compact tray's title row are the only triggers Announce needs, and per-profile Active toggles in the RowList are the canonical pause/resume mechanism. A separate "master enable" toggle was redundant: pausing every profile via their Active checkboxes is exactly the same outcome.
The dead FGI_MultiLineEditBoxSave custom widget (a "Save"-button-instead-of-"Accept" clone of AceGUI's MultiLineEditBox, introduced briefly for the inline-group profile message field) is also removed — no callers remain after the v2-tab move replaced multiline AceConfig inputs with a raw single-line InputBoxTemplate on the new strip.
Master enabled flag removed from the data model
DB.factionrealm.announce.enabled is gone — the only canonical pause mechanism now is the per-profile enabled flag (toggled via the On checkbox on each row of the Announce tab). Announce:Send() no longer consults a master toggle; it just iterates cfg.profiles. Horn-icon tooltips (compactFrame.lua and MainWindow.lua) dropped their "Master toggle is off" branch; the "no active profiles" branch is the only empty-state surface.
Wired up two horn-icon buttons
- compactFrame.lua line 397-402 — the title-row horn icon at
RIGHT,-92. Click invokesaddon.announce:Send(). TheOnEnterscript renders a dynamic tooltip viaaddon.announce:GetStatus()— one line per Active profile showingprofileName → Ready|in Ns. Gated byshouldShowTooltip()so the v2.0.7 "Disable compact UI tooltips" toggle still applies. Tooltip body points users at the new |cffffd700Announce|r tab on the main window for configuration. - GUI/MainWindow.lua line 385-411 — the bottom-row horn icon, anchored
BOTTOMLEFT, statusbg, BOTTOMRIGHT, 3, 2. Same click + tooltip pattern as compactFrame. Tooltip width bumped to 320 to accommodate the per-profile readout without wrapping. Tooltip body updated to point at the in-window Announce tab rather than the AceConfig sub-page.
New locale keys
29 announce* keys added to both Locale/enUS.lua and Locale/ruRU.lua. The chat-status print strings (announceMsgPostedTo, announceMsgAllOnCooldown, etc.) are the runtime-facing ones; the settings sub-page itself uses hardcoded English labels for consistency with the other sub-pages (Guild / Advanced / Messages all hardcode their labels). Russian translations are best-effort — missing keys fall through to the English value via the existing locale-lookup convention.
"What's New" popup overhauled to match the v2 main window's layout
intro.lua refactored end-to-end:
- Container switched from
ClearFrame→ stock AceGUIFrame(line ~65). ClearFrame is a title-bar-only variant defined in Libs/GUI.lua; the stockFramewidget ships the same title bar plus a bottom strip with a status-text label and a built-in Close button. The popup now matches the v2 main window's chrome (matching theMainWindow.lua:317pattern off:SetStatusText(addon.version)). - Status bar shows the version —
intro:SetStatusText("v" .. addon.version). Title bar simplified to just "Fast Guild Invite" since the version moved out. showLaterandshowNeverdismiss buttons removed. The old buttons each wrote a different value toDB.global.introShow: showLater stampedaddon.version(re-fire on next update), showNever stampedfalse(never again). The X close button on the AceGUI Frame now does what showLater used to via anOnClosecallback that writesDB.global.introShow = addon.version. The "never again" semantic is gone — users who want to permanently silence the popup can rely on the natural cadence (only fires once per addon version) or just close it./fgi introre-opens on demand.- Updates list is now a Blizzard
UIPanelScrollFrameTemplateinstead of an unboundedTLabel. The old layout auto-grew the label vertically to fit text, pushing the donation block off the bottom of the popup. New layout: scrollFrame anchored TOP tointro.head.frame.BOTTOMLEFTand BOTTOM tointro.body.frame.TOPLEFT(both with 10–20 px padding); a childFrame(the scrollChild) holds aFontStringwhose width matches the scrollFrame's clip area minus a 22 px gutter for the scrollbar. The scrollChild height is recomputed fromFontString:GetStringHeight() + 10so the scrollbar engages only when content exceeds the visible region. Scrollbar's right anchor inset 22 px so it doesn't crowd the popup's right border. - Donation block pinned to the bottom of the popup. The
intro.body(support text) is now anchoredBOTTOM = intro.paypalL.frame.TOP, 0, 10— directly above the topmost donation widget. The donation chain (paypal label, paypal field, discord label, discord field) sits flush above the AceGUI Frame's status bar / close button strip (offset y=50 from the bottom clears the 15 px status margin + 24 px status height + 11 px breathing room). Changes to the updates list now scroll inside the scrollFrame; the donation block never shifts. refreshUpdatesText()extracted as a shared function invoked by both thePLAYER_LOGINevent handler (existing trigger) and the newaddon.IntroShow()export (slash-command trigger). Lazily computes the scrollChild height each call.
Donation block simplified
Same file. Two donation entries removed entirely:
intro.streamelementsE+intro.streamelementsL("More options for support" → StreamElements URL).intro.patreonE+intro.patreonL(Patreon URL).
Remaining two entries updated:
- Discord URL bumped from
https://discord.gg/4mU5atttohttps://discord.com/invite/bY2R5TmBSz. - PayPal URL bumped from
https://www.paypal.me/Knoottohttps://www.paypal.com/donate/?hosted_button_id=XK7FEXTFAQPQ6(hosted donate button rather than a personal "/me" send-money page).
L.laterButton / L.neverButton locale strings and the btnText helper function deleted — no callers remain.
/fgi intro slash subcommand + addon.IntroShow() API
- FGI_Core.lua
Console:FGIInputgains anelseif str == 'intro'branch that callsaddon.IntroShow(). The on-login suppression flag (DB.global.introShow) is bypassed — explicit user invocation should show what they asked for. Listed in/fgi helpbetween thev2androsterlines. - intro.lua exports
addon.IntroShow = function() refreshUpdatesText(); intro:Show() end. Single entry point shared by the slash command and the help-icon click handler below.
Help-icon (i) on the v2 main window is now a click button
GUI/MainWindow.lua helpIcon block (line ~391) refactored:
CreateFrame("Frame", ...)→CreateFrame("Button", ...). Buttons inherit Frame's script bindings so the existingOnEnter/OnLeavetooltip handlers still fire.- Added
SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")so the icon picks up the same hover-glow the announce horn, compact-mode minus, and settings gear get — visually telegraphs the click affordance. - New
OnClickhandler callsaddon.IntroShow()if available (the same export/fgi introuses). BOTTOM_ROW_HELP[3](the line that describes the icon to itself) updated to read "Help — hover for the active tab's help (what you're reading right now). |cffffd700Click|r to open the |cffffd700What's New|r popup (same as |cffffd700/fgi intro|r)." so the click affordance is discoverable from inside the help tooltip itself.
Help tooltip width + auto-anchor fix
Same file, help-icon's OnEnter handler:
- Owner anchor:
GameTooltip:SetOwner(iconFrame, "ANCHOR_TOP")→addon.Tooltip.Owner(iconFrame)(per CLAUDE.md). The helper picksANCHOR_TOPRIGHTwhen the icon is in the bottom half of the screen (the common case for the bottom-right help icon, so tooltip rises up-and-to-the-left) andANCHOR_BOTTOMLEFTotherwise. Fallback toANCHOR_TOPLEFTifaddon.Tooltip.Ownerhasn't loaded yet. - Width:
SetMinimumWidth(420)→SetMinimumWidth(1200). The 420 value (the file default) caused every long help bullet to wrap 2-3 times, blowing the tooltip vertically to top-to-bottom of the screen. - Bug fixed: removed a
GameTooltip:ClearLines()call that was sitting afterSetMinimumWidth.ClearLinesresets the minimum-width setting back to the default, so prior attempts to raise the width were silently no-ops.SetOwneralready clears the tooltip implicitly so the explicitClearLineswas redundant anyway. Order now:SetOwner→SetMinimumWidth→AddLine× N →Show.
Non-ASCII character cleanup
WoW's default tooltip font and the addon's custom PT_Sans_Narrow font (used by the intro popup) don't carry every Unicode glyph. Several chars that rendered fine on the IDE side appeared as missing-glyph squares in-game. Audit done with Python script; replacements made in two files:
- intro.lua:
- 17 × U+2014 EM DASH (
—) →-- - 1 × U+2192 RIGHTWARDS ARROW (
→) →->
- 17 × U+2014 EM DASH (
- GUI/MainWindow.lua:
- 3 × U+25BE BLACK DOWN-POINTING SMALL TRIANGLE (
▾) removed from "|cffffd700Classes ▾ / Races ▾|r" (Filters tab help) and "|cffffd700Channels ▾|r" (Announce tab help). The triangle was decoration meant to indicate "dropdown" — the description text following each ("multi-select dropdown") already conveys the same info.
- 3 × U+25BE BLACK DOWN-POINTING SMALL TRIANGLE (
Cyrillic in Locale/ruRU.lua strings is left alone — those render fine on ruRU clients which load the Cyrillic font subset.
LibGuildRoster welcome-on-leave fix
User report: clicking the welcome message would fire when someone left the guild, not just on join. Root cause traced into the vendored Libs/LibGuildRoster-1.0/LibGuildRoster-1.0.lua:
- The lib's
OnChatMsgSystemhandler removes a leaver fromself.rosterimmediately onCHAT_MSG_SYSTEM "X has left the guild."so it can fireOnMemberLeftand clean up state. - A subsequent
GUILD_ROSTER_UPDATEevent sometimes carries a stale server roster snapshot that still includes the leaver (server-side roster takes a beat to propagate to the client). - The lib's diff:
wasOnlineis captured fromself.rosterAFTER the chat removal (leaver no longer in there). Rebuild fromGetGuildRosterInfo()returns the leaver because of the stale snapshot. Diff iterates new roster, sees leaver withwasOnline[leaver] == nil→ firesOnMemberJoinedfor the just-departed player → FGI's welcome callback in FGI_Core.lua:333 firesSendChatMessage("Welcome to the guild, NAME!", "GUILD")and the welcome whisper to the departing player.
Fix: added a lib.recentlyLeft = {} table (lib-scoped, in-memory) with lib.RECENTLY_LEFT_TTL = 60 seconds. OnChatMsgSystem's leave / kick branch stamps recentlyLeft[norm] = GetTime() alongside the roster wipe. OnGuildRosterUpdate's diff loop seeds wasOnline from recentlyLeft (entries within TTL get wasOnline[name] = false, treating the name as "existing" rather than "new"). Stale entries past TTL are evicted lazily during the same loop pass so the table can't grow unbounded across long sessions.
Annotated "FGI vendor fix" inline so a future re-vendor diff against the canonical lib is obvious. Side effect: a legitimate rejoin within 60 s of a leave won't fire OnMemberJoined for that specific rejoin — sub-minute rejoins are vanishingly rare so the trade is accepted.
Documentation updates
- docs/Curseforge_Description.html — added v2.0.9 entry at the top of Recent Updates; removed the oldest v2.0.4 section to keep the list at 5 patches per the CLAUDE.md convention.
- intro.lua
CURRENT_UPDATESarray — prepended the v2.0.9 bullet (in-game "What's new" popup) mirroring the Curseforge HTML language. - docs/v2.0-plan.md — Phase 9 status: complete. Only Phases 10 (Bindings.xml) and 11 (Polish + legacy file deletion + 2.0.0 version bump) remain on the v2.0 roadmap.
- docs/phase9-plan.md — implementation plan archived.
[v2.0.8] (2026-05-16) — LibGuildRoster Migration (Welcome-Spam Fix), Blacklist-Found Popup Improvements, Start Sync Button Feedback, Anti-Spam Sync Window Fix, Filter Name Required-Field UX, Statistics Period Dropdown Display Fix, Last/Next Scan Restored on v2 Scan Tab, D (Declined) Counter Added, Statistics Period Labels Clarified + 14-Day Option, Compact Tray Jitter Fix
Compact tray no longer jitters vertically when the queue changes
- Symptom (user report) — "when inviting the UI moves every time". Each invite, decline, or skip would visibly shift the compact tray up or down by a few pixels, making the tray feel unstable during normal recruitment.
- Root cause — the compact frame is created with
cf:SetPoint("CENTER", UIParent, "CENTER", 0, 0)in compactFrame.lua:76 and the fallback restore path at compactFrame.lua:716 also uses CENTER.cf.refreshcallscf:SetHeight(TITLE_HEIGHT + visibleRows * ROW_HEIGHT)at compactFrame.lua:696 on every queue mutation. With a CENTER anchor,SetHeightexpands or shrinks the frame symmetrically around the midpoint — so removing one row (ROW_HEIGHT = 16 px) moves the TOP down 8 px AND the BOTTOM up 8 px. The user sees the entire tray jump every time the queue changes by even one entry. - Fix — at PLAYER_LOGIN, after the saved position is restored (or the CENTER fallback is applied), capture the frame's current
GetTop()/GetLeft()and re-anchor asTOPLEFTrelative toUIParent BOTTOMLEFTusing those screen coordinates. After this,SetHeightgrows / shrinks the frame downward — the title bar stays put, only the bottom of the queue area moves. Idempotent: re-anchoring to the same coords on a session that already saved TOPLEFT is a no-op. The converted anchor is persisted toDB.global.compactFrameimmediately so subsequent/reloads use the new format directly without re-converting from a stale CENTER save. - One-time visual shift on first post-upgrade load — existing users with a saved CENTER anchor will see their tray's apparent centre shift slightly DOWN on first load (the captured screen coords are TOP of frame, which is
saved_CENTER_y + height/2). After that the tray stays put on every refresh. The shift is one ROW_HEIGHT/2 = 8 px at minimum if the queue is empty, up to half the full queue height. Acceptable tradeoff for eliminating per-invite jitter forever. - OnDragStop already saves whatever
cf:GetPoint(1)returns so post-conversion drags persistTOPLEFTautomatically; no change needed there.
Statistics period dropdown: labels clarified + "Last 14 days" added
- Labels were misleading — the old dropdown read
24 hours/1 week/1 month/All, which implied calendar windows (current week Mon-Sun, current month 1-N). The graph actually plots a rolling window fromnow - daysBacktonow. Relabelled toLast 24 hours/Last 7 days/Last 30 days/All timeto match the actual semantic. Last 14 daysadded between 7 and 30 since users asked for an intermediate window that the existing options didn't cover. New PERIODS entry{14, 14, 24}— 14 X-axis points, 1 per day, matching the 7-day option's points-per-day density.- Dedicated locale keys (
statsLast24h/statsLast7d/statsLast14d/statsLast30d/statsAllTime) introduced in Locale/enUS.lua and Locale/ruRU.lua instead of reusing the existingL["24 часа"] / L["1 неделя"] / L["1 месяц"] / L["Все"]keys. Those existing keys are shared with the Settings → "Clear DB after" dropdown, where labels like "1 week" are correct (absolute retention period, not a rolling window). Renaming them would have broken that dropdown's labels too. deDE / frFR / koKR / zhCN / zhTW locale files unchanged — non-English users see the English fallback for the new keys until a translator adds them, matching how the addon already handles missing locale entries. - Period-index migration — the PERIODS table grew from 4 entries to 5 (14 days inserted between 7 and 30), shifting the indices for 30 days (3 → 4) and All (4 → 5). Without migration, a user whose stored
prefs.periodwas 3 ("1 month") would post-upgrade see "Last 14 days" selected.StatisticsTab.MigratePeriodIndex(stats)in GUI/Tabs/Statistics.lua bumps the stored value to point at the user's previously-chosen window; gated byprefs.periodSchemaVersion = 2so the migration runs exactly once. Called fromFastGuildInvite:OnInitializein FGI_Core.lua so the index is correct before either UI (v2 tab or legacy v1 popup) reads it. The v2 tab'sRenderalso calls the same migrator defensively — it's idempotent. - Legacy v1 statistic popup also updated — same 5-entry
frame.connecttable and same locale keys in statistic.lua:463-475 so the popup graph stays in lockstep with the v2 tab. They shareDB.global.statistic.period; without this update the legacy popup's 4-entryconnecttable would have indexed past its bounds when the migration bumped a stored value to 5.
D (Declined) counter added to compact tray + v2 Scan tab
- Request — users asked for visibility into how many invites are being refused per session alongside the existing F (Found) / S (Sent) / A (Accepted) / X (Filtered) letters on the compact tray and v2 Scan tab counter strip.
- Source — combined
addon.searchInfo.decline(manual rejections, ≥ 1 s response) andaddon.searchInfo.autodecline(auto-decline addon hits, < 1 s response, classified by the v2.0.6 timing split). Both counters already existed and were already wired throughfn.historyand the Statistics tab graph as separate series; the new D letter shows their sum so the compact strip stays at one letter per outcome. - Implementation —
addon.searchInfometamethod's__callreturn extended from{unique, sended, invited, filtered}to{unique, sended, invited, filtered, decline+autodecline}so all three counter-display sites get the new value from the same source. Backward-compatible: existing callers thatunpack(t)to 4 vars (legacymainFrame.searchInfo.updateat mainFrame.lua:389) silently drop the extrat[5]. Display sites updated in compactFrame.lua:660 and GUI/Tabs/Scan.lua:280. - Colour —
|cffff6666(light red) chosen to read as "rejected" while staying visually distinct from the existing X orange (|cffff9966) — same negative-outcome family but enough hue difference that two adjacent counters don't blur into each other. - Tooltips updated — both counter hover regions now include a D line explaining it's combined manual + auto-decline and pointing to the Statistics tab for the breakdown. compactFrame.lua:210 (GameTooltip:AddLine) and GUI/Tabs/Scan.lua:539 (attachTooltip body).
- Legacy v1 main frame unchanged — the v1 frame uses the long-form locale string
L["Статистика поиска (краткая)"]("Found: %d Sent: %d Accepted: %d Filtered: %d") and only unpacks 4 values. Adding D there would mean editing the locale string in all 7 translations, which is out of scope for this change; the v1 frame is on the Phase 8 retirement path anyway.
v2 Scan tab: "Last scan / Next scan" line restored
- Symptom — the legacy v1 main frame had a centred line near the top of the scan area reading
"Last scan: <query> | Next scan: <query>"driven bymainFrame.luascanInfo. The v2 main window's Scan tab dropped that line entirely during the Phase 7 migration; users lost visibility into what query just fired and what's next in the queue. - Fix — added a
GameFontHighlightSmallfontstring to the v2 Scan tab between the top strip and the row list, sandwiched by a newSCAN_INFO_H = 18constant that therowsArea's top offset now accounts for. Stored aswidgets.scanInfoFssoScanTab.Refreshcan update it. - Refresh wiring —
ScanTab.Refreshnow readsaddon.search.lastQuery/whoQueryList/progressand builds the same"Last scan: X | Next scan: Y"string the legacy frame builds, including the(a)offset case frommainFrame.lua:402-433(betweenfn:nextSearchfiring and the WHO callback running,progressstill points at the just-fired query so the next slot is+1; outside that windowprogressalready points at the next slot). End-of-cycle wrap tolist[1]is preserved so the label doesn't blank out during the brief gap between the last WHO callback and the nextnextSearchcall. Locale keys reused (L["lastScan"],L["nextScan"],L["scanNone"]) so Russian / Chinese / Korean translations continue to work. - Fan-out trigger —
fn:nextSearchin functions.lua writesaddon.search.lastQuery = curQueryand was already calling the legacymainFrame.scanInfo.update()directly. Added anaddon.MainWindow.refreshScanTab()call right after so the v2 tab refreshes at the same moment as the legacy frame; both UIs now mirror lastQuery changes from one site. The existingfn.onListUpdateandaddon.searchInfometamethod fan-out paths already triggerScanTab.Refreshon queue/counter mutations, so the new explicit call only fills the lastQuery-changed gap they don't otherwise cover.
Statistics tab period dropdown now displays the selected period
- Symptom — clicking the period dropdown on the Statistics tab and picking 24 hours / 1 week / 1 month / All filtered the graph correctly (the period preference saved and the graph re-rendered) but the dropdown's visible button text stayed stuck on a placeholder string (rendered as
"Custom"on the affected client). The selection was effectively invisible. - Root cause —
makeDropdownin GUI/Tabs/Statistics.lua calledUIDropDownMenu_SetSelectedValue(dd, i)in both the initial render and the per-iteminfo.funcclick handler. That API only updates the internal selected-value state used byUIDropDownMenu_GetSelectedValue; it does not touch the dropdown button's visible text. The visible label on aUIDropDownMenuTemplateis set byUIDropDownMenu_SetText(dd, label)— and that call was missing from both code paths, so the button kept whatever placeholder text the framework had on it. - Fix — paired every
UIDropDownMenu_SetSelectedValuewith a matchingUIDropDownMenu_SetTextso the button label tracks the selection. Initial render now readsitems[startIdx]and callsSetTextonce with that label; the per-item click handler also callsSetText(dd, label)after the value-set so picking a different period visibly updates the dropdown immediately. - No functional change — the period filtering itself was always working (the
onSelect(i)callback fired correctly and wroteprefs.period = idx); only the display was broken.
Filters tab: Save button now greyed when Filter Name is empty
- Before — clicking Save on the Filters tab with the Filter Name field empty was a silent no-op (the
doSavehandler in GUI/Tabs/Filters.lua returned early viaif name == "" then return endwith no UI feedback). Users hit Save, nothing happened, and there was no indication that the name was the missing piece. - Fix — Save is now disabled by default and only enables when Filter Name contains non-whitespace text. Implementation:
nameInput:HookScript("OnTextChanged", refreshSaveEnabled)checks the trimmed input on every keystroke and callssaveBtn:Enable()/:Disable()accordingly; initial state isdisabledsince the input starts empty. Loading an existing filter via row-click (loadIntoForm) callsnameInput:SetText(name)which fires OnTextChanged → re-enables; saving and clearing the input fires OnTextChanged → re-disables. - Tooltip swaps body with state —
SetMotionScriptsWhileDisabled(true)keeps the hover tooltip firing while Save is greyed. AHookScript("OnEnter", ...)checksself_:IsEnabled()and shows either the regular Save-button help (when enabled) or"Filter Name is required — type a name in the Filter Name field before saving."in light-red (when disabled). Replaces the previous staticattachTooltip(saveBtn, ...)call. doSave's early return kept as belt-and-suspenders — the Enter-key submission paths (maxInput:SetScript("OnEnterPressed", function() doSave() end)and similar onmInput) can still invoke doSave directly. With the button disabled the OnClick path is dead, but pressing Enter in those inputs with an empty name still no-ops silently rather than crashing.
Anti-spam sync now honors the local "Clear DB after" retention setting
- Root cause —
fn.startSyncin functions.lua builtSync.tablesForSyncwith three of the four data tables callinggetLasWeekData(t, false, true)(the thirdtruemeans "full copy, no time filter") but the fourth —alreadySended(the anti-spam list) — calledgetLasWeekData(DB.realm.alreadySended)with nofullflag. That made the helper apply its 7-day time filter, hard-capping the synced payload at the last 7 days of anti-spam entries regardless of how long the user had been recruiting. Symptom: a guildie onboarded mid-recruitment received only the recent slice of anti-spam, missing weeks or months of older invited names. From git history (cfba887v1.6.3) the 7-day cap on alreadySended was deliberate when sync was first wired up, but the asymmetry was never updated when blacklist sync moved tofull=trueand when the local-retention settingclearDBtimeswas added. - Fix — sync window for
alreadySendednow reads fromDB.global.clearDBtimes(the existing Settings → Main → "Clear DB after" dropdown that controls local retention: never / 1 day / 1 week / 1 month / 6 months). The threshold seconds come fromFGI_RESETSENDDBTIME[clearIdx], the same constant that drives the local cleanup loop in FGI_Core.lua:737. Index 1 (never expire) sends the wholeDB.realm.alreadySendedtable; any other index sends entries newer thantime() - secs. The default (index 3 → 1 week) reproduces the pre-fix payload exactly, so users on defaults see no behavior change. - Implementation — inlined the windowed filter at the
Sync.tablesForSyncbuild site rather than threading a fourth parameter throughgetLasWeekData(the helper's name and the-7hardcoded day offset are misleading for the other three callers that bypass the filter viafull=true; widening its signature would compound that). The other three table entries (leave,blackList,blackListRemoved) still go throughgetLasWeekDataunchanged — they were already sending full copies and remain so. Protocol-compatible with prior versions: receivers don't care how the sender chose its keys. - Practical effect — recruiters who set "Clear DB after" to 1 month or 6 months will now sync that much anti-spam to onboarding guildies. Recruiters who set it to "never" will sync their entire list. ChatThrottleLib + the existing 255-byte chunking in
prepareTableForSendhandles the size increase; subsequent syncs to the same peer are deduplicated viaSync.cacheso the first onboarding sync is the only one that carries the full payload.
Welcome-spam fix v2: migrated join detection to LibGuildRoster-1.0
- First attempt (earlier in v2.0.8 dev) moved welcome firing from the GUILD_ROSTER_UPDATE diff in functions.lua to a
C_GuildInfo.GetGuildEventLog()"join" entry handler in FGI_Core.luaprocessGuildEventLog. Testing on retail showed it still spammed welcomes for every guildie who logged on, which the event-log approach shouldn't have done. Root cause: the retail copy of the addon was 10 days stale (the wow-version-replication watcher wasn't running), so the test was actually running v2.0.7's roster-diff welcome path the whole time. That's a process issue, not a code issue, but the event-log approach was still a roundabout fix. - v2.0.8 final: vendored
LibGuildRoster-1.0intoLibs/LibGuildRoster-1.0/and wired itsOnMemberJoinedcallback to fire welcomes. Source: the canonical lib inProfessionMaster/libs/LibGuildRoster-1.0/. The lib does a wipe-and-rebuild on everyGUILD_ROSTER_UPDATEand firesOnMemberJoinedwhen a name appears in the new roster that wasn't in the previous one. Same diff our code was doing manually, with three wins: one source of truth for joins across all WoW client versions, built-inwasInitializedguard so/reloaddoesn't welcome every existing member, built-in login-race retries whenGetNumGuildMembers()returns 0. - Two FGI vendor fixes applied to the lib (annotated "FGI vendor fix" inline so a re-vendor diff is obvious):
OnMemberJoinedwas dead code in the canonical lib — declared in the header docs but never:Fire'd anywhere. Added the firing insideOnGuildRosterUpdate's post-rebuild diff:wasOnlinecaptures the pre-wipe roster keyed by member name (true/false for online state); the diff loop now checkswasOnline[name] == nilto detect brand-new members (versuswasOnline[name] == falsefor existing-but-came-online), firingOnMemberJoinedandOnMemberOnlinerespectively. Purely additive — no existing subscribers to break since the callback never fired before.- Forced
SetGuildRosterShowOffline(true)at PLAYER_LOGIN on retail. Without this,GetGuildRosterInfo(i)iteration on retail is filtered by the guild panel's show-offline toggle — when off, the rebuilt roster only contains currently-online members and the presence-diff misfires whenever an offline guildie logs on (their name was absent from the previous filtered snapshot → looks "new" → falseOnMemberJoined). Gated to retail (WOW_PROJECT_ID == WOW_PROJECT_MAINLINE) because Classic/TBC/Wrath/Cata don't filter the iteration this way and mutating the filter on those versions would be a behaviour change for existing consumers (fastguildinvite is currently the lib's first retail use; other consumers likeProfessionMastership Classic-only). Lives inside the lib'sOnPlayerLoginso the toggle is set before its firstGuildRoster()call — otherwise the timing race against a separately-registered handler could deliver a still-filtered roster.
- Removed three now-redundant code paths:
processGuildEventLogin FGI_Core.lua reverted to leave-only — the v2.0.8-early "entry.type == 'join'" detection +seenJoinKeysdedup ledger are gone.- The Classic-only join-pattern match (
"^(.+) has joined the guild%.$") + welcome firing in FGI_Core.luaCHAT_MSG_SYSTEMhandler — replaced by the lib'sOnMemberJoinedwhich handles Classic via the same CHAT_MSG_SYSTEM →GuildRoster()→ rebuild chain. Leaving the chat-match in would double-welcome on Classic. - The retail
GUILD_ROSTER_UPDATEblock in functions.lua keeps its Accepted-counter /pendingInvites/rememberPlayerbookkeeping (idempotent and unrelated to the welcome path) but no longer fires welcomes — that block was already de-welcomed in v2.0.8's first attempt.
- TOC change:
Libs\LibGuildRoster-1.0\LibGuildRoster-1.0.luaadded to all five TOCs (FastGuildInvite.toc,_BCC,_Wrath,_Cata,_Mainline) right afterLibDBIcon-1.0'slib.xmlsoCallbackHandler-1.0(provided via the Ace3 dependency declared in every TOC) is available before the lib'sCBH:New(lib)call runs.
Start Sync / Sync now buttons now show in-place visual feedback
- Both Start Sync buttons (legacy popup
settings.lua syncNowAceGUI button, and v2 Settings panel Advanced → "Sync now" AceConfig button) now show their state on the button itself instead of relying solely on chat messages. Sequence on click:- Button label changes to
"Syncing..."and the button disables until the sync completes. - On success / nobody / failed / watchdog timeout, the button re-enables and shows a short result label for 5 seconds (
Synced with PartnerName (+N)/Synced with PartnerName (up to date)/Already up to date/Sync failed: PartnerName/(timed out)). - After 5 seconds the button returns to its idle label.
- Button label changes to
- Shared state lives on
addon.syncUI(settings.lua lines ~672-732):{ inProgress, manualClick, resultText, resultAt, setResult }.setResultis exported so the v2 panel's early-exit handler can route through the same writer (single owner for result-state changes, single owner for the 5 s clear timer and the 30 s watchdog). refreshSyncButtons()updates both surfaces (settings.lua line ~686): calls:SetText/:SetDisabledon the AceGUI button directly, and callsLibStub("AceConfigRegistry-3.0"):NotifyChange("FastGuildInvite")to re-render the v2 panel when it's open (no-op when closed). The AceConfig button'snameanddisabledfields are now functions that read live state fromaddon.syncUI.- Manual click overrides
muteSync/addonMSGfor chat output — the existingon*Sync*callbacks gated all chat prints onnot (addonMSG or muteSync). When the user explicitly clicks one of the Start Sync buttons, the newmanualClickflag forces a chat line on result too, so a muted user still sees confirmation that their action did something. Auto-syncs fromPLAYER_LOGINdon't set the flag and still respect the mute toggles.consumeManualClick()clears the flag in the final-state callbacks (onSyncSuccess/onSyncFailed/onSyncNobody) so the next sync defaults back to mute-respecting behavior. fn.startSyncnow returnsstarted, skipReason(functions.lua line ~3954):true, nilif the broadcast went out,false, "combat"if blocked byUnitAffectingCombat,false, "in_progress"ifSync.target ~= ''. The button click handlers use the return value to immediately surface "Skipped: in combat" / "Sync already in progress" on the button — previously these early exits were silent, leaving the user with no signal at all. Internal callers (functions.lua:516, 3312, 3939, 3988) still call without using the return values; behavior unchanged for them.- Watchdog timer — 30-second
C_Timer.NewTimerarmed whenonSyncStartedruns; ifinProgressis still true when it fires (e.g., callbacks dropped, sync state machine wedged), the button is force-reset with(timed out)text. Prevents the button from sticking in "Syncing..." indefinitely.
"Blacklisted X is in your guild!" popup now shows reason + has Unblacklist button
StaticPopupDialogs["FGI_BLACKLIST"]in blackList.lua (lines 313-398) extended with two improvements driven by the same user-visible dialog. The popup fires fromfn:blacklistKick()(functions.lua:927) on world entry and fromfn:blackListAutoKick()'sCHAT_MSG_SYSTEMhandler when a blacklisted player joins.- Reason now shown in the popup body —
showNext()looks upDB.realm.blackList[name]via the newlookupBlacklistReason()helper and appends"\nReason: <reason>"to the body text when an entry exists. The helper handles both the v2.0.5{ reason = string, time = epoch }table shape AND pre-v2.0.5 raw-string entries that haven't been re-saved since the migration. Lookup usesfn:fullPlayerName(name)as the primary key (matching howfn:blackListstores entries on connected realms) with a fallback to the bare name. Missing/empty reasons are silently omitted so legacy entries don't render"Reason: nil". - Third "Unblacklist" button added —
button1is still"Kick"→OnAccept(callsGuildUninvite),button3becomes"Skip"→OnAlt(just advances the queue; no action). Newbutton2 = "Unblacklist"→OnCancelcallsfn:unblacklist(name)so the user can forgive the player without leaving the dialog. Button-to-callback mapping follows Blizzard's documented 3-buttonStaticPopupconvention (button1=OnAccept, button2=OnCancel, button3=OnAlt).hideOnEscape = falseis retained so accidental Esc presses don't fire the middle-button unblacklist via the OnCancel-on-Esc path. - Layout — Visual order is
Kick / Unblacklist / Skip(left to right), reading as destructive → mid → passive so users can intuit the action without reading every label. All three branches mark the name indata2and advance to the next queued blacklisted player viashowNext()so the existing one-popup-per-player flow is preserved.
Welcome-spam fix v1 (superseded by the LibGuildRoster migration above, retained for context)
- Root cause — On retail,
GetGuildRosterInfo(i)iteration in theGUILD_ROSTER_UPDATEsnapshot diff is filtered by the guild panel'sSetGuildRosterShowOfflinetoggle. When that toggle is off (Blizzard default in many UI flows; other addons flip it), the stabilized snapshot only contained currently-online members. Previously-invited friends logging in later then appeared "new" in the diff and triggered the welcome path — once per invited friend that came online — producing the reported "spams welcomes for everyone invited in the session" behavior. v2.0.6 made this worse by writing every invited player toDB.realm.alreadySendedimmediately at invite time (the anti-spam fix), which increased the population of names eligible to misfire. - Fix — Retail welcome and whisper sending moved from the
GUILD_ROSTER_UPDATEdiff in functions.lua (line ~841) toprocessGuildEventLogin FGI_Core.lua (line ~278), keyed offC_GuildInfo.GetGuildEventLog()entries withentry.type == "join". This is the server-authoritative join signal we already use for"leave"/"remove"on retail, so the same event handler now produces both leavers and joiners in one pass. - Dedup — A module-level
seenJoinKeysset tracks the keys we've already welcomed; each entry's key ismemberName|year|month|day|hour. First call after login leavesjoinersempty (theseenJoinKeysnil-check inside the loop guarantees that) so existing log entries don't all welcome on/reload; the snapshot saved on that first call becomes the baseline that subsequent calls diff against. Stale keys for members who later left and rolled off the server-side log are naturally evicted when the snapshot is replaced. - Roster-diff path retained for bookkeeping only — The
if gv.isRetail then ... GUILD_ROSTER_UPDATEblock in functions.lua (line ~756) still ownsfn.history:joined(),addon.searchInfo.invited(),fn.history:onAccept()/logInvite("accepted"),addon.pendingInvitescleanup, and therememberPlayerfallback. Those operations are idempotent (rememberPlayerandonAccepton a name we've already processed are no-ops) so the filter quirk doesn't break the Accepted counter the same way it broke welcomes. - Roster map simplified — Since
originalName(the realm-suffixed form the welcome path needed for cross-realm whispers) is no longer used in this block,currentis now keyed[normalizedName] = trueinstead of[normalizedName] = memberName. The unusedoriginalNamevalue in the diff loop was removed. - Classic/TBC/Wrath/Cata unchanged — Non-retail versions never had this bug because they trigger welcomes from the
CHAT_MSG_SYSTEM"X has joined the guild." pattern match in FGI_Core.lua (line ~209), which fires exactly once per real join with no roster snapshot involved. That path stays as it was.
[v2.0.7] (2026-05-06) — Compact UI Tooltips Disable, /fgi v2 Respects Open-Last-Used
Compact UI tooltip disable toggle
- New
Disable compact UI tooltipscheckbox in General → Appearance settings (default off). When enabled, all hover tooltips are suppressed on the compact tray — resize grip, scan counters (F/S/A/X), title icons (gear, help, announce, plus, close), row icons (invite, decline, blacklist, skip), the invite-next button (+(N)), and the scan button (>>). The compact frame itself remains fully functional; this only suppresses the tooltip popups. Useful for distraction-free recruiting when you already know what each icon does. - Implemented via
shouldShowTooltip()helper in compactFrame.lua (line ~16) that returnsfalsewhenDB.global.compactTooltipsDisabledis true. All 9OnEnterhandlers in the file checkif not shouldShowTooltip() then return endbefore callingaddon.Tooltip.Owner/GameTooltip:SetText. Lines 116, 202, 246, 300, 350, 462, 524. - DB default
compactTooltipsDisabled = falseadded to FGI_Core.lua (line ~595). AceConfig toggle added to GUI/SettingsPanel.lua Appearance section (order 16, afteropenLastUsed). Lines 613-621.
/fgi v2 now honors openLastUsed setting
/fgi v2(and/fgi v2.0) now routes through the samepickOpenView()logic as/fgi showso theOpen last-used viewtoggle is honored consistently across all open commands. WhenopenLastUsedis enabled andlastOpenedViewis"compact",/fgi v2opens (or toggles) the compact tray instead of always opening the v2 main window. When off, falls back to thecompactModepreference like before.- Updated in FGI_Core.lua slash command handler (lines 821-837). The picker returns
"compact"or"main"; compact path togglesinterface.compactFramevisibility, main path callsaddon.MainWindow:Toggle().
[v2.0.6] (2026-05-06) — Declined Invites History Fix, Retail Decline Detection Restored, Auto-Reject Timing Detection, Scan Interval Retail Fix
Anti-spam list fix: all invite types now call rememberPlayer() immediately
- Types 1, 2, and 4 now call
fn:rememberPlayer()immediately when the invite is sent, matching the Type 3 pattern that was already working. Previously only Type 3 (Message Only mode) would write toDB.realm.alreadySendedupfront; Types 1, 2, and 4 deferred the anti-spam write to the decline/accept handler, which meant if the server confirmation never arrived (or the event was missed) the player would never be remembered. - All four invite paths in functions.lua
invitePlayer()now writeDB.realm.alreadySended[normalizedName] = trueimmediately after callingaddon.API.GuildInvite(), before any message send or sync broadcast. Location: lines 1660-1730. - Manual invite paths updated: FGI_ChatMenu.lua (both legacy UIDropDownMenu and modern Menu API paths) and FGI_Core.lua whisper reply dropdown now call
fn:rememberPlayer()and populateaddon.pendingInvitesimmediately afterGuildInvite(). Lines 64-77, 219-233, 115-130. - Removed redundant
rememberPlayer()calls from Scan.lua decline/auto_decline handlers — the player is already in the anti-spam list from the upfront write, so the event handler only needs to clearpendingInvitesand log to History.
Retail decline detection restored via CHAT_MSG_SYSTEM with pcall wrapper
- Re-enabled
CHAT_MSG_SYSTEMevent on Retail (Scan.lua line 128). v1.9.9 disabled it to avoid taint errors from Blizzard's "secret strings" (patterns likeERR_GUILD_DECLINE_Sthat exist in the client but triggerSetForbiddenSecretstaint when read by addons), but decline/accept messages only arrive viaCHAT_MSG_SYSTEM— theUI_ERROR_MESSAGEevent doesn't carry them. - Wrapped
playerHaveInvite(msg)inpcall()on Retail so tainted strings are caught and silently ignored without breaking execution. Thesuccess, type, name = pcall(playerHaveInvite, msg)pattern returnsfalsefor tainted patterns; the handler checksif not success then return endand skips processing. Lines 145-180. UI_ERROR_MESSAGEhandler unchanged — still registered on Retail as a fallback, but most invite responses now flow through theCHAT_MSG_SYSTEMpath with the taint guard.
Timing-based auto-reject detection
addon.pendingInvitestable restructured from[normalizedName] = playerName(string) to[normalizedName] = { name = playerName, time = GetTime() }(table) to track when each invite was sent. All write sites updated: functions.lua Types 1/2/4, FGI_ChatMenu.lua context menu paths, FGI_Core.lua whisper reply. Lines 1660-1730, 64-77, 219-233, 115-130.- Accept handlers in functions.lua updated to handle both old string format (for backward compatibility with in-flight invites from before the change) and new table format. Lines 701-720, 821-840:
if type(pending) == "table" then name = pending.name else name = pending end. - Decline handlers check elapsed time since invite send:
GetTime() - pending.time < 1.0classifies as auto-reject (player has an auto-decline addon installed),>= 1.0classifies as manual decline. Auto-rejects callfn.history:onDeclineAuto()and log with outcome"antispam"; manual declines callfn.history:onDecline()and log with outcome"declined". Lines 145-180 (Retail CHAT_MSG_SYSTEM), 227-265 (Classic/UI_ERROR_MESSAGE). - Classic and Retail both use the same timing check — the pattern matching in
playerHaveInvite()checksERR_GUILD_DECLINE_AUTO_SbeforeERR_GUILD_DECLINE_Sso explicit auto-reject messages still take precedence, but when both patterns would match (Retail sends the generic decline message for both outcomes) the timing heuristic provides the classification.
Retail scan interval fix: libWho retailConfig now respects user setting
fn.setScanInterval(n)in functions.lua now callslibWho:SetRetailConfig("interval", n)on Retail in addition tolibWho:SetInterval(n). Lines 2865-2872. The LibWho_Retail.lua override returnsretailConfig.intervalfromGetInterval(), completely bypassing the value set bySetInterval()— so the user's configured scan interval was being ignored and the timer always showed 8 seconds.- Added initialization in
PLAYER_LOGINhandler (functions.lua lines 3993-4003) to callfn.setScanInterval(DB.global.scanInterval)once the database loads, ensuring the retailConfig value is synced with the persisted user setting on every login. LibWho_Retail.luaunchanged — the existingSetRetailConfig(key, value)method (lines 279-284) was already present but never called by the addon. Now it's wired into the settings flow so the user's preference propagates to the Retail-specific config table.
Backed out AFK detection feature
- Removed
CHAT_MSG_AFKandCHAT_MSG_DNDevent registrations from Scan.lua line 129-130. - Removed AFK/DND auto-reply handler (lines 208-237) that marked
pending.isAFK = trueand set a 90-second timeout to remove non-responders from the anti-spam list. The feature was an edge case — AFK players are online and can receive invites normally; the timeout logic added complexity without clear user benefit.
[v2.0.5] (2026-05-06) — Blacklist Reason Input + Timestamps, Open-Last-Used Toggle, Scan-Button Safety, Case-Insensitive Sort, Checkbox Revert Fix
Blacklist reason-input dropdown
- Replaced the legacy Yes/Cancel blacklist confirmation with a free-text reason input. Every blacklist gesture in the addon — compact tray row icon, v2 Scan tab row icon, chat right-click
FGI - Blacklist— now opens a small dropdown at the cursor with a title row (Blacklist <name>), anEditBoxfor the reason, OK / Cancel buttons, and a hint line showing the configured default reason. Empty input falls back to that default. HittingEnterinside the input commits;Escapecancels. Single shared helper in GUI/UI.luaUI.ShowBlacklistConfirmso all three surfaces share identical UX. The legacyFGI_V2_BLACKLIST_EDITStaticPopup remains for the Blacklist tab's edit-existing-row flow. Fast blacklisttoggle still gates this — when on, the dropdown is skipped and the silent default-reason path runs (UI.FastBlacklist); when off, the new reason-input dropdown opens.- Implemented via UIDropDownMenu's
info.customFrame. A persistent 80 px tallFramehosts the EditBox + buttons + hint fontstring, mixed in withUIDropDownCustomMenuEntryMixin(with inline stubs for older clients lacking the global) so the dropdown framework'sSetOwningButton/GetPreferredEntryHeightcalls succeed. Pre-built at file load to avoid first-open construction races. - Auto-close timer disabled. WoW's
UIDROPDOWNMENU_SHOW_TIME(~2 s mouse-out timer) doesn't care that an EditBox child has focus — it'd fireCloseDropDownMenusmid-typing. Three-shot timer kill (synchronous + next-frame + 100 ms) directly nilsDropDownList1.showTimer / isCountingand removes theOnUpdatescript.OnEditFocusGainedre-kills counting on every focus regrab to defend againstOnLeavere-arming viaStartCounting. - First-open layout primer. The framework's listFrame mis-sized on the very first open with a customFrame (~80 px of empty space below the row); subsequent opens were correct. Open-then-close-invisibly primer with
DropDownList1alpha-zeroed at an offscreen anchor warms the framework state so the user's first real open lands on the correct layout.
Blacklist timestamps + sortable Added column
DB.realm.blackListvalue reshape:name -> reason(string) is nowname -> { reason, time }(table). New entries stamptimewith the current epoch; on edit-reason the existing add-time is preserved (you're rewriting the reason, not re-adding the player). Idempotent migration in FGI_Core.luaOnInitializeconverts legacy string values to{ reason = str, time = 0 }(0 = "unknown, predates the v2.0.5 timestamp feature"). Sync receive coerces incoming string values from older peers to the new shape on the local side so reads stay uniform.- All write sites updated —
fn:blackList, GRM importer'stryAdd, GIL importer'stryAdd, legacyblackList.luaOnAcceptedit path. Sync ships the full table value automatically (existingupdateTableForSyncplumbing). - All read sites updated — officer-chat blacklist message,
!blacklistGetListdebug print, legacyblackList.luaOnShow+update()display loop, v2 Blacklist tabStaticPopup OnShow, and the v2 Blacklist tab'sbuildRowsdata builder. - New
Addedcolumn in the v2 Blacklist tab, between Name and Reason, fixed-width 120 px, sortable. Renders asYYYY-MM-DD HH:MMfor stamped entries and—for legacy entries withtime = 0. TheYYYY-MM-DDformat is lexicographically chronological so RowList's string sort gives the expected oldest-first / newest-first ordering; the em-dash sorts after digits so unknown-time rows naturally sink to the end in ASC.
"Open last-used view" toggle
- New
Open last-used viewcheckbox in General → Appearance settings (default off). When on, the minimap left-click and/fgi showopen whichever view (full main window or compact tray) was used most recently — so a user who switches to the compact tray with the−button can now "live" in compact without having to flip thecompactModepreference manually. - Persistence is always-on. The compact tray's
+(expand) button writesDB.global.lastOpenedView = "main"and the main window's−(compact-mode) button writesDB.global.lastOpenedView = "compact"on every click, regardless of the toggle. Cheap to track and means flipping the feature on later doesn't need a primer click — the most recent gesture is already recorded. - Router refactor.
addon._pickOpenView()in FGI_Core.lua returns"main"or"compact"based onopenLastUsed+lastOpenedView(when on) orcompactMode(otherwise). BothmainFrameToggle(minimap LMB) andfn.showAddon(/fgi show) route through it for consistency.
Scan-button safety timer (compact tray + v2 Scan tab)
- Rare-but-real "frozen scan button" bug fixed on both the compact tray and the v2 Scan tab. The optimistic visual cooldown (
cf.scanCooldown = true,widgets.scanCooldown = true) set on click had no escape hatch — if libWho'stimeCallbackStart/timeCallbackEndnever fired (because /who was rejected, libWho was busy with another addon, or a partial scan abort), the cooldown flag stayedtrueforever andif cf.scanCooldown then return endsilently no-op'd every subsequent click. - Mirrored the v1 main window's safety pattern. Each click increments a
cooldownGencounter and schedulesC_Timer.After(libWho:GetInterval() + 5, ...). The deferred callback re-enables the button only whencooldownGenstill matches (so click N's still-pending timer can't prematurely clear click N+1's cooldown) and the flag is still set. Same defensive invariant added in v1.9.9 for the v1 main button. - v2 Scan tab also gained the optimistic visual cooldown for parity with the compact tray (it was previously missing the click-debounce, exposing the same WHO_LIST_UPDATE round-trip window).
Tooltip auto-flip — prefer ABOVE
- (continued from v2.0.4) The
addon.Tooltip.Ownerglobal helper was already updated to preferANCHOR_TOPRIGHTwhen there's ≥250 px of screen space above the frame. The compact tray's help "i" tooltip gotSetMinimumWidth(500)to flatten its vertical extent so it stays under that 250 px reserve.
Case-insensitive sort across every RowList column
GUI/RowList.luacomparator now lowercases both sides for the primary compare so"Apple"/"apple"/"banana"/"Banana"group together instead of scattering by ASCII byte order (Lua's default string compare is case-sensitive — capitals 65–90 sort before lowercase 97–122). Falls back to the original case-sensitive compare on tie so equal-modulo-case strings get a stable, deterministic order.- Applies to every sortable column on every v2 tab — Blacklist, Anti-Spam, Custom Scan, Filters, Quiet Zones, Statistics, etc. Reported originally for the Blacklist tab's Reason column.
Checkbox visual-revert fix
- Checking / unchecking a row's checkbox in any v2 list-editor tab no longer requires a tab away + back to "stick". RowList's checkbox click handler previously dispatched to the column's
onTogglecallback (which writes to DB) but never updatedentry[col.key]on the row data — so any subsequent_renderRows(parent resize, scroll, sort header click, externalRefresh) repainted the cell from the stale value and visually reverted the click. Fixed by writingentry[col.key] = valin the framework's click handler before dispatching, so the row data stays in sync with the visible state and every re-render shows the user's click. - Applies to every checkbox column in the addon — Custom Scan's
On+Strict, Filters'On, scan-group enable toggles, anywhere elsecheckbox = trueis set on a column spec.
Sync output respects muteSync
- GRM and GIL import-done chat lines now honor
DB.global.muteSync(functions.lua). Both importers are sync-class operations (pulling foreign blacklist data into the local realm) so the same flag that suppresses peer-sync chatter now suppresses the importer's completion line. The on-screenaddon.API.ShowMessagebanner still fires either way — that's a transient notification, not chat spam, and it's how mute-on users still find out the import finished.
[v2.0.4] (2026-05-05) — Phase 8 Sub-pages, Compact Tray Resize, Skip/Decline Semantics, Intro Popup Fix, FGI Chat Submenu, Graphical Progress Bar
Settings panel — Phase 8 sub-pages filled out
- Guild sub-page populated with the full set of settings from
guild.lua: auto-welcome to guild chat + welcome message body, auto-whisper new members + whisper body, auto-blacklist guild leavers, announce blacklist additions in officer chat, and Classic-only set-public-note + set-officer-note with their respective templates (gated behindgv.isRetail-falsehiddencallbacks because Retail's note-setting APIs are Blizzard-only). - Advanced sub-page populated: Anti-spam memory (remember skipped, anti-spam expiry, history retention), Sync (print/mute sync chat, GRM/GIL auto-sync),
/whochat output (show /who results, show invite system messages — both per-realm), Debug (debug mode, scan logs, show update info), Manual triggers (Trigger version check button, Sync Now button). - Messages sub-page populated as a list-style editor for whisper templates:
selectdropdown showing all templates with truncated bodies as labels,multiline = 6input editing the selected template's body, Add new + Delete current execute buttons. Live storage inDB.factionrealm.messageList/curMessagematches the legacymessage.luashape so both UIs share data. - Section headers redesigned with hover-tooltips. Each sub-page's section heading was previously a
type = "header"followed by atype = "description"body block (visible inline wall-of-text). Replaced by a single new custom AceGUI widgetFGI_TooltipHeader: brand-coloured centred title flanked by divider lines, with the section'sdescfield surfaced as a hover tooltip via the sameOnEnter/OnLeavedispatch pattern AceConfigDialog already uses. Eleven section headers across General, Guild, Advanced, Messages converted; the inline description blocks went away. - Per-input field labels also got hover-tooltips via a sibling widget
FGI_TooltipInput: clones AceGUI's stockEditBoxwidget but moves the tooltip surface from theEditBoxitself to an invisible Button frame overlaying the label fontstring's bounds. Hovering the label shows the tooltip; hovering the typing area doesn't pop a tooltip while you're trying to edit. The four Guild text inputs (welcome message, welcome whisper, public note, officer note) use it.
Settings panel — reorganization
createMenuButtonstoggle moved from Advanced → Debug to General → Invite behaviour under the new labelAdd FGI to player right-click menus. The toggle gates the v2.0.3 native chat-menu integration; burying it in Debug made it undiscoverable. The new tooltip enumerates the surfaces (chat, friends list, party / raid frames, guild roster), the three follow-on actions (Guild Invite / Blacklist / Unblacklist), and how to reach the same actions when off (slash commands, v2 main window row icons)./whoscan subdivision moved from General to Advanced. Power-user setting, doesn't belong in the General sub-page.
Settings panel — clearDBtimes semantic restoration + crash fix
- The
clearDBtimessetting was rendered as a 0-365 daysrangeslider in v2.0.3, but the field is actually an INDEX into a 5-elementFGI_RESETSENDDBTIMEtable ({ 0, 86400, 604800, 2592000, 15552000 }— disable / 1 day / 1 week / 1 month / 6 months in seconds). Anyone who interacted with the slider could write a value above 5, which then crashedOnInitializeatFGI_RESETSENDDBTIME[DB.global.clearDBtimes]withattempt to compare nil with number. The crash aborted the rest ofOnInitializebeforefn:initDB()ran, soDBanddebugDBupvalues infunctions.luastayed nil — every downstream call (fn.startSync,FiltersUpdate,fn.debug) then hit nil-DB errors. - Fixed in GUI/SettingsPanel.lua by replacing the slider with a
selectdropdown{ [1] = "Never expire", [2] = "1 day", [3] = "1 week", [4] = "1 month", [5] = "6 months" }. Thegetcallback clamps any out-of-range saved value back to3(1 week, the default). - Hardened in FGI_Core.lua — the cleanup loop now clamps
idxto[1, #FGI_RESETSENDDBTIME]before indexing, writing the clamp back to DB so future runs see a valid value, and gates the threshold lookup onsecs and secs > 0so a 0-second (disable) or nil entry no-ops cleanly.
Anti-spam memory — Skip / Decline semantic restoration
- Decline now always adds the player to the anti-spam list. Previously functions.lua:1663 gated the anti-spam write behind
if noInv and (DB.global.rememberAll or DB.global.rememberSkipped), so with both flags off a Declined player could reappear on the next scan. The flag check is gone; thenoInvpath always remembers — that's the whole point of Decline (vs Skip). The v2 Scan tab and compact tray's row icons both flow through this path so they get the new behaviour for free. - Skip now respects
rememberSkipped— previously the v2 row icons' Skip onClick calledtable.remove(list, idx)and never touched therememberPlayerpath or therememberSkippedflag. The flag was misnamed (it actually gated the Decline path, alongsiderememberAll). Both v2 Scan tab and compact tray Skip handlers now readDB.global.rememberSkippedafter thetable.removeand callfn:rememberPlayer(entry.name)when on. Off by default; Skip stays a soft removal. rememberAllflag retired — the field was always functionally identical torememberSkipped(both gated the samenoInvpath). Removed fromDB.globaldefaults, the Wago Analytics switch, the AceConfig Advanced sub-page, the legacy popup checkbox + its SV-load setter, and the locale layout-size entries inruRU/zhCN/zhTW/summary.lua.- The
rememberSkippeddescription on the Advanced sub-page rewrites to spell out the Skip-vs-Decline distinction so the toggle's purpose is unambiguous.
Security sub-page retired
- The legacy
security.luapanel had two toggles (DB.global.security.sended/.blacklist) that were never read by any sync code — write-only flags that did nothing. Deleted the file, removed thesecurityfield fromDB.globaldefaults, removed the AceConfig Security sub-page (renumbered Advanced/Announce/Messages/Credits to fill the gap), removedsecurity.luafrom all 5 TOCs, and dropped the three dead locale strings (Безопасность,Подтверждение отправки данных синхронизации,Список отправленных приглашений) from all 7 locale files.docs/v2.0-plan.mdupdated to mark the sub-page retired with rationale.
Compact tray — resizable
- Width is now user-resizable via an invisible grabber in the bottom-right corner. At rest no chrome shows (the tray's minimal aesthetic is preserved); on hover three faint white dots fade in arranged in a diagonal grip pattern, plus a
ResizeGameTooltip explaining what to do.OnMouseDowncallscf:StartSizing("RIGHT")so only width changes — height stays driven by the queue refresh.OnMouseUpsavescf:GetWidth()toDB.global.compactFrame.widthand the PLAYER_LOGIN restore reads it back with sanity bounds (>= MIN_WIDTH and < 3000). - Hard floor at MIN_WIDTH = 300 so the title-row counter strip always has at least ~80 px to render typical mid-scan readouts (
F:25 S:15 A:3 X:7) on a single line. Below this point the counters used to wrap into stacked lines becauseGameFontHighlightSmalldefaultsSetWordWrap(true). - Counters now use
SetWordWrap(false)/SetNonSpaceWrap(false)/SetMaxLines(1)so even at extreme scan-state values the text clips off the right rather than wrapping into a multi-line block that breaks the title-row layout. Same single-line clipping pattern the queue rows already used for the lvl/class column. LVLCLASS_Wreduced from 110 to 90 px in the queue rows. Was sized for"60 Demon Hunter"(Retail) at ~105 px; Classic Era's longest is"60 Warrior"at ~70 px. The 20 px goes back to the name field at every frame width — names truncate later in every queue row, including at the new 300 px minimum width.- Resize bounds set via direct
cf:SetResizeBounds(MIN_WIDTH, MIN_HEIGHT)with acf:SetMinResizefallback, inlined rather than calling throughaddon.UI.ApplyMinResizebecausecompactFrame.lualoads BEFOREGUI/UI.luain the TOC andaddon.UIdoesn't exist yet at that point. Same logic the helper wraps, just inline. - Help "i" tooltip on the compact tray now includes
Move:andResize:lines so the resize affordance is discoverable through the existing help icon.
Intro popup — silently broken since launch
- The "Show update info on login" toggle was wired but the popup never displayed for any user, ever, due to two compounding bugs in
intro.lua's PLAYER_LOGIN gate:tonumber(FGI.version) == nilrejected every 3-segment version string —tonumber("2.0.3")returns nil because Lua's number parser doesn't accept multiple decimal points. The popup was always suppressed for any real release.L.updates = {}in bothenUSandruRUlocale tables was hardcoded empty, and the#L.updates == 0guard short-circuited the display logic regardless.
- Fixed: removed the
tonumberguard entirely (the existingDB.global.introShow == addon.versionequality check handles dev builds correctly becauseaddon.versionis the literal"FastGuildInvite-v2.1.12"string in dev — first-time stamp matches, suppressing future shows). PopulatedCURRENT_UPDATEStable with six user-facing v2.0.4 bullets shared between both locales (Russian translations TODO). - Updated
CLAUDE.mddocumentation-rule to addintro.lua'sCURRENT_UPDATESarray to the per-release update list alongsideCHANGELOG.mdanddocs/Curseforge_Description.html. The Curseforge HTML's "Recent Updates" section andCURRENT_UPDATESshould mirror each other — same audience, same plain-language voice.
User feedback — ElvUI dropdown spacing on TBC + global dropdown helper
- A user on TBC Classic with ElvUI reported a visible gap between the per-item radial slot and the label text in the Quiet Zones tab's continent / zone dropdowns. Default WoW dropdowns reserve a left-side check-mark slot for
notCheckable = nilitems; ElvUI's dropdown skinning pads this slot into a visible gap. Most addon dropdowns (Quiet Zones continent + zone, Statistics period, blacklist Yes/Cancel, chat right-click follow-on, Custom Scan add-member, Clear-confirmation, legacyInitMenu) are single-select and don't use the check-mark slot at all — selected value is shown viaUIDropDownMenu_SetText. - New global helper GUI/UI.lua
addon.UI.CreateMenuInfo([checkable])— a thin wrapper aroundUIDropDownMenu_CreateInfo()that defaultsinfo.notCheckable = true. Every dropdown call site that doesn't need the check-mark slot now goes through this single helper, so the ElvUI fix is one-and-done and any future dropdown automatically inherits the right defaults. Passtrueto opt back into the slot (Scan tab's Mode dropdown still usesinfo.checkedfor active-mode indication and is left untouched). - Migrated call sites: GUI/UI.lua
ShowBlacklistConfirm, FGI_ChatMenu.lua (3 items), FGI_Core.luaInitMenu(5 items), GUI/Tabs/Scan.lua Clear dropdown, GUI/Tabs/CustomScan.lua add-member dropdown, GUI/Tabs/QuietZones.lua continent + zone dropdowns, GUI/Tabs/Statistics.lua period dropdown.
Quiet Zones tab — master toggle surfaced
- The legacy settings popup carried an
Ignore quiet zonescheckbox that flippedDB.global.quietZones— the master gate read byIsInQuietZone()in functions.lua:421. When off, the scan engine bypassed both the built-in instance filter (raids / dungeons / arenas / battlegrounds viafn.getStaticAreas()) AND the user's custom zone list. The toggle never made it into the v2 main UI during the Phase 4 list-editor migration, so users couldn't disable filtering without going to the legacy popup. - New
Filter quiet zonescheckbox at the top-right of the GUI/Tabs/QuietZones.lua strip. Tooltip explains it's the master switch — when off, EVERY zone (built-in instance list + the custom rows below) is bypassed for the scan engine. Toggle callsfn.getAreas(true)to invalidate the area cache so the change takes effect on the next scan tick. MIN_WIDTHbumped from 540 → 640 px so the new toggle's checkbox + label (~150 px) doesn't overlap the Add button at the smallest window size.
Tooltip anchor — prefer ABOVE the frame
addon.Tooltip.Ownerpreviously placed tooltips BELOW the frame when the frame was in the top half of the screen (anchorANCHOR_BOTTOMLEFT) to avoid clipping off the screen top, ABOVE only when the frame was in the bottom half. Compact-tray testing flagged that "below" routinely covered the queue rows underneath the title-row icons — exactly the content the user was trying to read.- Flipped to prefer ABOVE. The helper now checks
GetScreenHeight() - frame:GetTop() > 250(a conservative 250 px reserve for the tallest tooltip the addon renders) and usesANCHOR_TOPRIGHTwhen there's room above; falls back toANCHOR_BOTTOMLEFTonly when the frame sits so close to the screen top that an above-anchored tooltip would clip. Global change — every call site (compact tray, Scan tab, Settings panel checkboxes, RowList action icons, etc.) gets the new behaviour without per-site edits.
Compact tray — help "i" tooltip widened
- The compact tray's help icon tooltip is the longest in the addon (10 lines covering icons / counters / move / resize affordance), so at GameTooltip's default ~250 px width it rendered nearly half the screen tall. With the new "prefer ABOVE" anchor logic, that meant any tray docked closer than ~250 px from the screen top fell back to the BELOW path again — covering the queue.
makeTitleIconnow takes an optionaltooltipWidthargument that callsGameTooltip:SetMinimumWidth(N)beforeSetText. The help "i" passes 500, which flattens its body from ~10 visual lines to ~5-6 — well under the 250 px reserve so the global helper can keep anchoring above the tray even when docked near the screen top. Other compact-tray tooltips (settings, close, etc.) are short single-sentence things that fit the default width without modification.
FGI chat right-click menu — hover-open submenu instead of click-popup
- The v2.0.3 native chat-menu integration added an
FGIentry to WoW's right-click menus that, on click, opened a follow-onUIDropDownMenuat the cursor with the three actions (Guild Invite / Blacklist / Unblacklist). User feedback: the click-popup pattern dismissed the parent menu and forced a separate close interaction; preferred a standard hover-open submenu that stays nested in the parent and lets the user navigate away naturally. - FGI_ChatMenu.lua refactored to use a Menu API submenu.
rootDescription:CreateButton("FGI")with no click callback auto-promotes the button to a submenu with Blizzard's native>arrow; the three children attach viaparentDesc:CreateButton(...)so the framework owns the open/close lifecycle (no manual timer or close). - Per-item tooltips preserved via the modern Menu API's
description:SetTooltip(callback)— each child's tooltip body sits on apcall-guardedattachMenuTooltiphelper so a future patch dropping:SetTooltipjust loses the tooltip body, not the whole submenu. Tooltips render viaGameTooltip_SetTitle/GameTooltip_AddNormalLinewhen those globals exist (modern clients) and fall back to directtooltip:SetText/tooltip:AddLinecalls otherwise. - Legacy
buildFollowOn/ShowFollowOnpaths kept intact — FGI_Core.lua's SetItemRef hook still callsShowFollowOnas the fallback for clients whereMenu.ModifyMenudidn't attach.
Main window — graphical scan progress bar restored
- The v2 status bar text rendered scan progress as ASCII (
#filled,.empty) inside the AceGUI Frame's bottom statustext fontstring. User feedback flagged that the bar visibly "grew in width" as more#replaced.—#is meaningfully wider than.in WoW's proportional GameFont, so the perceived bar width changed with progress. The legacy v1mainFrame.luarendered progress via an AceGUIProgressBarwidget whose colored texture grew left-to-right inside a fixed-width frame; users preferred that aesthetic. - Replaced the ASCII bar with a colored Texture parented directly inside the AceGUI Frame's
statusbg. Same v1 pattern (Libs/GUI.lua:1507): texture anchored TOPLEFT / BOTTOMLEFT with 4 px padding and width =(statusbg:GetWidth() - 8) * (done/total)set on every progress tick. Drawn on theARTWORKlayer so it sits beneath the OVERLAY-layer statustext fontstring — version + percent + queries text reads on top of the orange fill (#FF8000@ 40% alpha). - Texture cached on
statusbg.fgiProgressTexso reopen reuses the existing instance instead of creating duplicates on the recycled AceGUI Frame (textures don't haveSetParentlike frames; we can't detach on close). MainWindow:SetScanProgress(done, total)is the public method called by GUI/Tabs/Scan.lua's update tick alongsideSetStatusText. Hides the texture when idle / scan finished; sets width and shows when scanning.- First implementation tried a floating
StatusBarabove statusbg — invisible because the AceGUI tab-content frame sits at the sameMEDIUMstrata with a higher frame level and occluded the 6 px strip. Switching to an in-statusbg texture sidesteps strata fights entirely (texture renders on the parent's draw layers). - Status text simplified — was
vX.Y | Scanning [###....] 51% | 23/45 queries, nowvX.Y | Scanning 51% | 23/45 queriessince the bar provides the visual readout.
fn.debug self-healing
fn.debugpreviously read from a localdebugDBupvalue assigned only byfn:initDB(). WhenOnInitializeaborted partway through (theclearDBtimescrash above was one trigger)fn:initDB()never ran anddebugDBstayed nil. Subsequentfn.debugcalls then errored withbad argument #1 to 'insert' (table expected, got nil), masking whatever the actual upstream problem was.- Fixed:
fn.debugnow readsaddon.debugDBlive as a fallback (local sink = debugDB or addon.debugDB) and gates eachtable.insert(sink, ...)onsinkbeing non-nil. Worst case it skips the SV log line and just prints to chat — the function never errors, even on a partially-initialised addon state.
[v2.0.3] (2026-05-05) — Scan Groups, Native Chat Menu, Notification Fix, Subdivision Toggles
Notification banner — silently broken since legacy
- Queue-notify and Scan-ready alerts now actually render.
FGI.animations.notification:Start(text)was a long-standing no-op for two compounding reasons. (1)font = font or settings.Fontresolved to nil becauseL.settingsonly definessize = {...}—SetFont(nil, 21, "OUTLINE")then silently failed and left the FontString unrenderable. (2) The hosting frame was created viaCreateFrame("Frame")with no parent and no explicit strata, ending up at defaultMEDIUMwith the FontString on the lowestBACKGROUNDdraw layer — covered by basically any other addon overlay or chat frame even when the font HAD been set. Rebuilt: parented toUIParentatFULLSCREEN_DIALOGstrata, FontString promoted toOVERLAYdraw layer withGameFontHighlightLarge, font fallback chainfont or anim.f.font or STANDARD_TEXT_FONT(the captured-at-init font path that the original author meant to use), and a fade-in → hold → fade-out animation group so the banner is visible for a clear ~3.5 seconds instead of snapping into a 0.5 s tail-end fade after a 3 s invisible delay.
Settings panel
- General sub-page expanded with Scanning (interval slider + level-range priority dropdown), Appearance (window opacity slider + minimap icon + keep open on Esc), Invite behaviour (auto-kick blacklist + Fast blacklist), Notifications (queue notify + scan-ready alert), and
/whoscan subdivision (see below). - Section headers use the native AceConfig
type = "header"style — centered label between two horizontal divider lines. Triedtype = "description"with brand-colour wrap andfontSize = "large"first (visual consistency with the orange tab labels) but reverted: the divider lines actually read as section breaks; left-aligned colored text didn't. - Discord invite link in Credits is non-editable but selectable. Custom AceGUI widget
FGI_ReadOnlyEditBoxcloned from stockEditBox: blocksOnCharso typed characters never insert, and snapsOnTextChangedback to the canonical URL as a defence-in-depth net for backspace / delete / paste. Mouse selection still works so users can Ctrl+A / Ctrl+C the URL. Replaces the previous "snap-back on focus loss" pattern that visibly flickered when typed in. - Gear-icon click no longer closes the v2 main window.
Settings.OpenToCategory()callsCloseSpecialWindows()which fires theFGIMainWindowEscProxyOnHide handler → closes the v2 frame. Fixed by temporarily nilling that OnHide before the open call and restoring on the next frame viaC_Timer.After(0, ...).
/who scan subdivision toggles
- Per-tier on/off in General settings.
DB.global.subdivideLvl / subdivideRace / subdivideClass / subdivideZone(all default true). Each tier has its own toggle in the General sub-page under a new "/who scan subdivision" header. Disabling a tier means capped queries at that shape accept the truncated 50 and the chain stops there — section description spells out "Tiers run in fixed order: Level >> Race >> Class >> Zone" with a "you will miss players" warning. - Dispatch in
searchWhoResultCallbackderives the tier from query shape, not justsearchLvl. Multi-level query (max > min) at any sLevel halves viaLVLsplitand is gated onsubdivideLvl. Single-level queries are gated on the per-tier flag matching theirsearchLvl(race / class / zone).willSubdividefactors in the gate so progress-weighting math stays correct when a tier is off (cap accepted → full worst-case cost credited so the bar advances).
Blacklist confirmation flow
- Blacklist row icons (v2 Scan tab + compact tray) honour
DB.global.fastBlacklist. Previously the row icons silently bypassed the toggle: the v2 Scan tab always openedFGI_V2_BLACKLIST_EDITregardless of fastBlacklist; the compact tray always blacklisted silently regardless of fastBlacklist. The toggle is now the single switch for "do I get prompted or not" across every blacklist gesture (chat right-click, slash command, v2 Scan row icon, compact tray row icon). - Confirmation popup replaced with an in-place Yes/Cancel dropdown at the cursor. Two new shared helpers in
addon.UI:ShowBlacklistConfirm(entry, onDone)opens aUIDropDownMenuwithYes, blacklist <name>(tooltip showing the default reason) andCancel;FastBlacklist(entry, onDone)runs the silent path (blacklist with default reason, log "blacklisted" history, drop from queue, fireonListUpdate, refresh). Both v2 Scan tab and compact tray row-icon onClicks branch onfastBlacklistto call one or the other. Per-player free-form reasons are now reached only via the Blacklist tab's edit popup; the row-icon path always uses the default reason. SettingsPanel.luaFast blacklist description rewritten to describe the actual surface area (row icons + chat right-click + slash command) and the off-state behaviour (the new Yes/Cancel dropdown).
Native chat right-click menu integration
- New file FGI_ChatMenu.lua registers a single "FGI" item in WoW's native chat right-click menu via
Menu.ModifyMenuagainst 12 unit-popup tags (MENU_UNIT_PLAYER,MENU_UNIT_FRIEND,MENU_UNIT_CHAT_ROSTER, etc.). Each tag's callback adds aCreateButton("FGI", ...)that, on click, opens a follow-onUIDropDownMenuat the cursor with three tooltipped items:FGI - Guild Invite,FGI - Blacklist,FGI - Unblacklist. The follow-on lives in its own dropdown rather than as a UnitPopup nested submenu because Blizzard's Menu API doesn't pass per-item tooltips in a portable way across patches. - Legacy
SetItemRefhook +addon.MENUretired on clients where native attach succeeded.FGI_Core.lua's OnEnable checksaddon.ChatMenu._registeredModernand only installs the chat-link fallback hook when the native injection didn't take. Eliminates the dual-popup the user flagged ("I get both the FGI popup AND the in-game popup"); on Classic Era 1.15.x the native path attaches all 12 tags so users see a single integrated menu. - Diagnostic
addon.ChatMenu.PrintDiagnostic()prints which tags accepted the registration to chat — useful for future Blizzard tag renames. - Confirmed working on Classic Era 1.15.x which has fully migrated off the legacy
UnitPopupButtons/UnitPopupMenustables; the legacy code path was tried during early development and removed once the modern API was confirmed working everywhere.
Scan Groups (Custom Scan tab)
- New file FGI_ScanGroups.lua owns the entire scan-groups feature. Multi-membership (the same scan can live in any number of groups) with per-membership enable independent of standalone
selected. Members are an ordered list of customScans names; a group can be reordered manually via per-member up/down arrows. Strict-sequential execution: when a group is enabled, every query (including subdivision children) of member N drains before member N+1 starts. Multiple enabled groups also drain strict-sequentially: group A finishes, then group B starts. - Schema —
DB.faction.scanGroups = [{name, enabled, expanded, members = [{scan, enabled}, ...]}, ...]. MirrorsDB.faction.customScansshape. No migration; defaults to empty. - Scheduler refactor (
fn:nextSearch) — branches intoaddon.ScanGroups.PopulateQueuewhenaddon.ScanGroups.HasActiveGroups()returns true, otherwise runs the existing customScans + default-sweep path unchanged.addon.search.bucketQueueholds the ordered bucket list (one bucket per enabled group with at least one resolvable enabled member, plus a final standalone bucket containing customScans-with-selected=trueminus dedup against grouped scans, plus the default sweep when on).whoQueryListis the ACTIVE bucket's queries; subdivisions push into it directly so children stay scoped to the active bucket. When the active bucket drains (progress > queueLen),AdvanceBucketreplaceswhoQueryListwith the next bucket's queries; after the last bucket cycles,PopulateQueuerebuilds from current state for a fresh pass (so user changes mid-scan get picked up).fn.clearSearchcallsScanGroups.Resetto wipebucketQueue/bucketIndexso a fresh scan after Clear starts clean. - Bucket-transition announcements gated behind
DB.global.logs.on— same flag that controls the per-query "Search returned N players" line. Each bucket activation prints<FGI> now running group X (N queries)(or... standalone scans (N queries)) so users with logs enabled can verify strict-sequential drain in real time. - UI on the existing Custom Scan tab — strip gains an
Add Groupbutton on row 2 alongsideDefault Scan. Body splits into two areas: a Groups area at the top (height 0 when no groups exist, so users without groups see the same layout they had before) and the existing scansRowListbelow it. Each group renders as a header row[On] [+/-] Name [+ Add member] [× Delete]; expanded groups show indented member rows underneath[On] scan name [^] [v] [× Remove].Add Memberopens aUIDropDownMenulisting customScans not already in the group; clicking one appends it as an enabled member. Reorder via the per-row up/down arrows. Group rows live in their own ad-hoc Frame layout (not viaRowList) so the hierarchical render shape doesn't pollute the shared component. - Standalone scans header label — brand-coloured
Scanslabel appears above the existing scans list when at least one group exists, for visual separation. Hidden when no groups. - Cascade cleanups on customScan delete / rename —
fn.groupsCleanScanwalks every group'smembers[]and drops references to a deleted scan;fn.groupsRenameScanwalks every group and updatesmembers[*].scanfrom old name to new. Wired into the existing customScan Delete action and the rename branch ofdoSave. - Save button renamed
Save→Save Scanfor visual disambiguation from the newAdd Groupbutton (both pull from the same Scan Name input field). Scan Name label updated toScan / Group Namewith a tooltip listing both creation paths and what each requires. - Defensive quote-strip in
trim— pasting a name like"60 Casters"(with literal quote characters, common when copying from chat / prose) creates a scan/group called60 Casterswithout the quotes. - Layout fix in
rowsAreaanchoring. RowList's_recomputeVisibleRowsreadsparent:GetHeight()to size its visible-row pool — whenrowsAreawas anchored TOPLEFT togroupsAreaBOTTOMLEFT (a dynamically-sized intermediate Frame), GetHeight could return 0 in some layout-pass timings, leaving zero visible rows even whencustomScanshad data. Switched to parent-relative anchoring with an explicit y-offset computed from group state, plus an explicitrl:Refresh()after every layout change. Standalone scans now render reliably regardless of group count or expand state.
[v2.0.2] (2026-05-04) — RowList Overhaul, Sort Arrows, Edit-Mode Rename, Gear Icon
Major Changes
- GUI/RowList.lua column-system rewrite — propagates to every list-style tab (Scan / Custom Scan / Filters / Blacklist / Anti-Spam / History / Quiet Zones). Single edit applies to all seven tabs:
- Sort arrows on every sortable header —
Interface\Calendar\MoreArrowtexture (the Blizzard chevron the calendar's guild-events list uses on its own column headers, lifted from ClassicCalendar'sCalendarEventInviteSortButtonTemplate). ASC/DESC swap by flipping VTexCoordtop↔bottom (SetTexCoord(0.0, 0.9375, 0.0, 0.6875)for DESC,(0.0, 0.9375, 0.6875, 0.0)for ASC). Arrow position computed fromGetStringWidth()so it always sits 3 px right of the actual rendered text — never lands inside an adjacent column regardless of how the column was specced. - Header text always LEFT-justified; data cells always LEFT-justified.
col.justifystill accepted in the spec for back-compat but ignored at render time. Layout reads uniformly across header and rows. :Newpre-pass bumps eachcol.widthto at leasttextWidth + 22 px(3 gap + 15 arrow + 4 right pad) so the sort arrow always fits inside the column without spilling. Uses a hidden probeFontStringonUIParentto measure each header's actual rendered width.- Checkbox columns LEFT-align the box at the column's left edge (was centred — looked off after the LEFT-justify pass since header "On" / "Strict" hugged the left while the boxes floated in the middle of their column slot).
_buildRow+_buildHeaderrewritten to respect column-array order. Find the (single) auto-width column index; columns BEFORE it chain LEFT-to-LEFT fromparent.LEFT + LEFT_PAD; columns AFTER chain RIGHT-to-RIGHT fromparent.RIGHT - iconAreaW; the auto column fills whatever's between the two chains. Previously the auto-width column always landed leftmost regardless of array position, which forced consumers to put auto-width columns first; now they can sit anywhere in the array._makeHeaderColumngot a richeroptsarg with three modes:{leftOffset = N}for LEFT-chain,{rightOffset = N}for RIGHT-chain,{leftOffset = N, rightOffset = M}for auto-width.
- Sort arrows on every sortable header —
Tab consequences of the RowList overhaul
- Filters tab (GUI/Tabs/Filters.lua) — column order is now On / Name / Classes / Races / Lvl / Count + delete-action (was On / Name / Lvl / Classes / Races / Count). Classes is the auto-width flex slot so it grows with the window.
- Custom Scan tab (GUI/Tabs/CustomScan.lua) — the On checkbox column is now actually leftmost visually (was hijacked by the Parameters auto-width column under the old layout).
- Edit-mode rename for both Filters and Custom Scan — clicking a row sets
editingName; Save updates THAT row, renaming it if the Name input differs, instead of creating a new entry under the new name. Carries On / Strict / Count state across the rename. Editing-the-row-then-deleting-it clearseditingNameso the next Save creates new instead of failing on a missing target. - Dead
justify = "RIGHT"/"CENTER"/"LEFT"specs cleaned out of every RowList consumer — they no longer affect rendering after the LEFT-justify changes above.
Scan tab
- Two-zone wheel-scroll on the Lvl readout — replaces the single-zone slider that scrolled both bounds together. New layout
Lvl [min] - [max]with each number an independently-scrollable Button-frame hover zone. ±1 normal, ±5 with Shift. Push-partner clamp: scrolling Min past Max bumps Max up to match (and vice versa) so the range can never invert. Hover highlight on each number-button so the cursor target is unambiguous. Mirrors the legacy v1 spinner mechanics. - Tooltip body cleaned of internal
DB.global.lowLimit / highLimitnames and Phase 8 references per CLAUDE.md guidance ("users don't care").
MainWindow bottom-row icons
- Announce + compact-mode minus icons resized to 20×20 (was 14×14) to match the AceGUI Close button's height. Help "i" stays 24×24 per user feedback. All five icons' vertical centres line up at y=27 (close: 17+10, help: 15+12, the three 20-tall icons: bottom y=17 + height 20 → centre 27). Status bar's right edge moves from -191 to -226 to fit the wider icons + the new gear with even 3-px gaps between every element.
- New gear icon between the minus and Close button. Texture:
Interface\Icons\Trade_Engineering. Click opens the legacy settings popup viainterface.settings:Show() / .ShowContent("Main"). Tooltip notes that Phase 8 will swap the click handler over to the AceConfig settings panel (Settings.OpenToCategory("FastGuildInvite")on retail,InterfaceOptionsFrame_OpenToCategoryon classic) and retire the legacy popup. - Help-icon tooltip's
BOTTOM_ROW_HELPappendix gains a fifth line for the gear with a|TInterface\Icons\Trade_Engineering:14:14|ttexture escape so the rendered icon shows inline alongside announce / help / minus. local interface = addon.interfaceadded to the file preamble — without it the bottom-row icon click handlers (the compact-mode launcher specifically) blew up with "attempt to index global 'interface' (a nil value)" becauseinterfacewas being treated as a global instead of a file-local. Sameaddon.interfacereference the rest of the v2 GUI files use.
Compact frame
- Announce button mirrors the v2 main window's bottom-row horn — same
Interface\Icons\INV_Misc_Horn_03texture, same Phase 9 placeholder click handler, same tooltip pointing at ESC > Options > AddOns. Shifts pause/scan fromRIGHT,-48to-70and invite from-72to-94to make room between expand (+) and pause (>>); counters' right edge moves from-110to-132. - Drag fix on the counter tooltip overlay. The Button covering the wide middle of the title bar (
counterTip, the F/S/A/X tooltip-bearing region) was intercepting the title bar'sRegisterForDrag("LeftButton")events becauseEnableMouse(true)is implicit on Buttons. Users couldn't move the tray by click-dragging the counter region — the drag silently no-op'd because the events never reachedtitle's drag handler. Fixed by also callingcounterTip:RegisterForDrag("LeftButton")and forwardingcf:StartMoving()/:StopMovingOrSizing()with the same DB-position-save body as the title bar's drag handler.
[v2.0.1] (2026-05-04) - TBC / Wrath / Cata / Mainline TOC Sync
Bug Fixes
/fgi v2reported "v2.0 main window not loaded" on TBC / Wrath / Cata / Mainline clients. When v2.0.0-beta shipped, the v2.0 GUI files (GUI/UI.lua,GUI/RowList.lua,GUI/Tabs/*.lua,GUI/MainWindow.lua) were added to the Classic Era TOC (FastGuildInvite.toc) but the four sibling TOCs (FastGuildInvite_BCC.toc,_Wrath.toc,_Cata.toc,_Mainline.toc) carried the v1.x file list unchanged. On those clientsMainWindow.luanever loaded,addon.MainWindowstayed nil, andConsole:FGIInputat FGI_Core.lua:753 printed the error and returned. Fixed by replicating the same v2.0 GUI block (afterinviteHistory.lua, beforedebug.lua) into all four sibling TOCs so the load order matches the Classic Era TOC. All five TOCs now share the same 12-file GUI list.
[v2.0.0] (2026-05-04) - v2.0 UI Overhaul
Major Changes
- New tabbed main window replaces the popup-soup main UI. Built TOGProfessionMaster-style with an AceGUI Frame + TabGroup root and per-tab modules under GUI/Tabs/. Reachable via
/fgi v2; the legacy/fgi showwindow keeps working in parallel where feature parity isn't yet 100 % (Phase 8 retires the legacy window). Eight tabs ship: Scan, Filters, Blacklist, Anti-Spam, History, Statistics, Quiet Zones, Custom Scan. - Standardised data-row component — GUI/RowList.lua. Distilled from
compactFrame.lua; every list-style v2 tab consumes it instead of building its own row layout. 16 px row height, alternating row banding, class-coloured names, right-edge action icons (Invite / Skip / Decline / Blacklist textures fromInterface\RaidFrame\ReadyCheck-*andInterface\Buttons\UI-GroupLoot-Pass-Up), virtual scrolling with mouse-wheel + slider, optional column headers with click-to-sort (asc/desc), optional checkbox-column type wired to a per-rowonToggle(entry, val). The scrollbar is a plainCreateFrame("Slider")with manual textures rather thanUIPanelScrollBarTemplatebecause the template's modern secure-scroll-template logic crashes on a non-ScrollFrameparent.
Tabs
- Scan tab (GUI/Tabs/Scan.lua) — primary working surface. Single-row strip in compact-frame style:
>>scan /+(N)invite /Clear/ Mode dropdown /F:n S:n A:n X:ncounters / wheel-scrollableLvl X-Yreadout. Below the strip, RowList queue with per-player Invite / Skip / Decline / Blacklist icons mirroring the compact tray's per-row layout exactly. Status text on the AceGUI Frame's bottom bar shows version when idle, prepends scan progress while a/whois in flight (vX.Y | Scanning [#####.........] 36% | 12 / 24 queriesin plain ASCII — the unicode block characters tried first didn't render in WoW's default font chain). Scan button greys out and shows seconds remaining during the libWho cooldown via the samesetMainScanCooldownfan that drives the compact tray's countdown. - Filters tab (GUI/Tabs/Filters.lua) — whitelist semantics. v2 filters answer "who do I want?" (selecting Shaman means I want Shamans), not "who do I exclude?".
DB.realm.filtersListentries gain aschemaVersion = 2marker pluswantedClasses/wantedRaceswhitelist tables;fn:filteredandisQueryFilteredbranch onschemaVersionso v2 entries use whitelist logic and pre-existing v1 deny-list filters keep working unchanged. Inline form: Filter Name + Min/Max Lvl + Save on row 1, Classes ▾ + Races ▾ multi-select dropdowns + Min RIO M+ + Raid + N/H/M kill counts on row 2. The legacyDB.realm.enableFiltersmaster switch is no longer gated at any of the four query-pruning sites — per-filterfilterOnis the only switch that should matter; the master flag was a v1-era kill switch that surprised v2 users when their per-row toggles did nothing. - Blacklist tab (GUI/Tabs/Blacklist.lua) —
RowListview ofDB.realm.blackListwith name + reason + per-row delete + click-to-edit. Top strip carries Name + Reason + Add inputs and Import GRM / Import GIL buttons; both importers reuse the existingfn:importGRMBlacklist/fn:importIgnoreListBlacklistpaths. Edit popup uses the modern WoW StaticPopup template'sself.Text/self.EditBoxfield names (the lowercase v1 names crash on current clients) and supportsdata.removeFromQueue = trueso the Scan tab's row blacklist icon can reuse the same popup to ask for a reason and drop the player from the queue on Save. - Anti-Spam tab (GUI/Tabs/AntiSpam.lua) —
RowListview ofDB.realm.alreadySended. Pre-formattedYYYY-MM-DD HH:MMtimestamps so the column sorts chronologically without needing a separate sort key. Clear All button with confirm popup. - History tab (GUI/Tabs/History.lua) —
RowListview ofDB.factionrealm.history.invites, newest first, with class-coloured names and outcome colour codes (accepted=green / declined=red / antispam=orange / blacklisted=purple). - Statistics tab (GUI/Tabs/Statistics.lua) — LibGraph-2.0 chart of historical events plus all-time / per-session totals. Per-series visibility checkboxes + period dropdown.
- Quiet Zones tab (GUI/Tabs/QuietZones.lua) — cascading Continent + Zone dropdowns sourced from
C_Map.GetMapInfo/GetMapChildrenInfo; pre-selects the player's current location on first open. Catalog walks once per session and caches. - Custom Scan tab (GUI/Tabs/CustomScan.lua) — runs as a scan launcher with named profiles, not just a
/who-string list.DB.faction.customScans = [{name, query, selected, strict}, ...]with a per-realmDB.realm.defaultScanSelectedtoggle. Inline Scan Name + Scan Parameters + Save form on the strip; Save creates or updates by name, preserving On/Strict. Mid-dev migration inOnInitializeflattens any priorqueries[]shape into per-query profiles and folds the legacycustomWhoListinto named "Imported N" entries. - Locations tab dropped — replaced by automatic zone subdivision inside the scan engine.
functions.lua locationSplitreads zones directly from the just-returned/whoresults instead ofDB.factionrealm.locations, so depth-3 50-cap queries always subdivide by zone without any user-curated list. - Messages tab dropped from the v2 main window — message templates are settings, not interactive data; they migrate to the AceConfig settings panel in Phase 8 alongside the auto-welcome-whisper text and the Announce per-channel messages.
Scan engine
fn:nextSearchbuilds the work queue from selectedcustomScansprofiles plus (whendefaultScanSelected) the level-band sweep. Per-query flags (addon.search.queryFlags[query] = {custom, strict}) tell the result callback whether to subdivide on a 50-cap (per-profile strict) and whether to fire the "50+ results" warning (custom-profile queries only). Falls back to the level-band sweep when nothing is selected so the Scan button never silently no-ops.fn:getEffectiveLevelRange()resolves the default-sweep level range based onDB.global.levelRangePriority. Two sources can drive it: the strip range (DB.global.lowLimit / highLimit, edited via the wheel-scrollable Lvl readout) and the union of active v2 filters' lvlRanges. Priority defaults to"strip"; Phase 8 surfaces the toggle in the AceConfig General sub-page. Filters' level constraints still post-filter on level for any filter that has one — strip controls where to look; filters control who passes.fn:filteredsplit into v1 deny-list and v2 whitelist branches keyed offschemaVersion.isQueryFilteredunderstands v2 filters too, soqueryWorstCaseCostprunes the worst-case denominator correctly when a v2 class/race filter is active (fixes the +1, +1, +1, +89 progress-bar jumps users hit when running a v2 class filter — the v1 path only inspectedclassFilter/raceFilter, neverwantedClasses/wantedRaces).fn.clearSearch()extracted as a UI-agnostic helper so the legacymainFrame's localclearSearchand the v2 Scan tab's Clear button delegate to one place. Wipes the queue, the in-session anti-spam cache, and the F/S/A/X session counters via thesearchInfometamethod (n=0 zeroes); leavesDB.realm.alreadySended(the persistent anti-spam list) alone.
UI plumbing
addon.UI.ApplyMinResize(frame, minW, minH)— wraps modernSetResizeBoundswith aSetMinResizefallback. Each tab module exportsMIN_WIDTH/MIN_HEIGHTconstants based on its own content; MainWindow'sOnGroupSelectedapplies the active tab's floor on every tab switch and auto-grows the window when the current dimensions are below the new floor.- Bottom-row icon strip — three icons aligned with even 3-px gaps between the status bar's right edge, the AceGUI Close button's left edge, and each other: announce horn (Phase 9 placeholder, brass-horn texture from the Blizzard icon atlas), help "i", compact-mode minus (
UI-MinusButton-Up/Down). Help-icon tooltip appends a global "bottom-row icons (visible on every tab)" section using|T<path>:14:14|ttexture escapes so the rendered icons appear inside the tooltip — avoids the unicode glyph problem. - Compact frame — added an X close button to the title row's rightmost position; OnShow refreshes counters and queue rows so a scan run from the v2 main window populates the tray immediately when it's opened. Counter strip got a tooltip overlay explaining the F/S/A/X abbreviations (mirrored to the v2 Scan tab so both views read the same). Expand-to-full button now opens the v2 main window instead of the legacy
mainFrame. searchInfometamethod andonListUpdatebroadcast toaddon.MainWindow.refreshScanTabwhen set, so any counter or queue mutation propagates to the v2 Scan tab without the engine needing to know about it specifically. The legacymainFrame.searchInfo.updateis now nil-checked so it can be retired in Phase 8 without the metamethod crashing.- Map-press fix — the v2 ESC proxy joined
fn.updateEscFramesso the existingDB.global.keepOpentoggle covers the v2 window too. Pressing M (which callsCloseSpecialWindows()) no longer hides the v2 window whenkeepOpenis on. - AceGUI Frame pool-leak fix —
OnClosedetaches every manually-attached child icon (_helpIcon,_announceIcon,_compactModeIcon) from the AceGUI Frame before release. Without it the pool reused the widget on the next:Open()with the old icons still attached, so fresh icons rendered on top of leftovers.
[v1.9.10] (2026-05-02) - Retail Welcome-Spam Fix + Scan-Button Crash Fix
Bug Fixes
- Scan
>>button raised "attempt to index global 'libWho' (a nil value)" on every click. The v1.9.9 OnClick safety-timer fix at mainFrame.lua:735 callslibWho:GetInterval() + 5to size the safety timer — butlibWhois only declared as a local in functions.lua:12 (local libWho = LibStub("FGI-WhoLib")), and locals don't cross files. mainFrame.lua never had its own local declaration, so the reference resolved to the nil global. Crash fired on every scan-button click in v1.9.9; fixed by addinglocal libWho = LibStub("FGI-WhoLib")to mainFrame.lua's preamble alongside the existingGUILibStub local. The fix went unnoticed in pre-release testing because the source-of-truth tree lives in_classic_era_and the user's retail install hadn't been replicated since the v1.9.9 commit landed. - Retail: auto-welcome no longer spams a flood of welcome messages on a single guild join, while still firing reliably for every real join. The v1.9.6-era retail welcome path is a GUILD_ROSTER_UPDATE roster-diff handler in functions.lua — it keeps an in-memory snapshot of the guild roster and welcomes any member that appears in the live roster but not in the snapshot. The diff was added because retail's CHAT_MSG_SYSTEM payloads for guild-event messages are tagged as "secret strings" —
msg:matchagainst them either raises a taint error or returns no match silently (chat frames have privileged C-side access addons don't share, which is why the message renders in the player's chat but is unreadable from a Lua handler). The v1.9.10 first attempt to switch to CHAT_MSG_SYSTEM withpcallconfirmed empirically that real "X has joined the guild" messages didn't fire the welcome path on retail (and the accept counter, which lived on the same handler, also stopped updating). - The diff approach was fragile against retail's roster batching, which is what caused the original spam:
- Initial-load race. Retail streams the guild roster in over multiple GUILD_ROSTER_UPDATE events after login or
/reload. The v1.9.x first event captured a partial roster as the baseline snapshot — if it was missing N members (typical when recently-joined alts sit at the bottom of the roster and the first batch truncates before them), the next event treated those N as "new joiners" and welcomed them all. - Mid-session roster reload. Anything that triggers a fresh
C_GuildInfo.GuildRoster()call (FGI's own peer sync, GRM/GIL imports, other addons) restreams the roster in chunks and produces the same partial-then-full diff, spamming welcomes for already-known members.
- Initial-load race. Retail streams the guild roster in over multiple GUILD_ROSTER_UPDATE events after login or
- v1.9.10 keeps the diff (it's the only addon-readable signal for guild joins on retail) and replaces the v1.9.x first-event snapshot with stabilization: while the snapshot isn't yet "stable" the handler silently absorbs every event into the snapshot without firing welcomes, the joined counter, or the accept counter. The snapshot is declared stable after
STABLE_THRESHOLD = 2consecutive events report the same member count — at that point initial roster batching has settled and any subsequent count growth is a real join. - After stabilization, mid-session roster reloads triggered by sync/import are absorbed correctly: even if
ndrops during the reload's transition (early-out keeps the snapshot intact), when it climbs back the diff finds zero new members because the locked snapshot already matches the post-reload roster. Real joins during a stable window grownpastsnapshotSizeand produce one new member each, firing exactly one welcome message and one whisper per join. - A first v1.9.10 attempt added a 30-second login grace window and a burst threshold (suppress welcomes when >2 "new" members appear in one event) on top of the diff, but it traded one bug for another — multiple invitees accepting close together hit the burst threshold and got zero welcomes. Both the grace window and the burst threshold are gone in the final shipped version; stabilization replaces them.
- Classic / TBC / Wrath / Cata are unaffected — they continue to use the CHAT_MSG_SYSTEM "X has joined the guild" path which fires only on real game system messages and was never susceptible to the roster-batch race. The
if not gv.isRetailgate in the functions.lua CHAT_MSG_SYSTEM handler and the early-return on retail in FGI_Core.lua's leave/welcome handler stay in place.
[v1.9.9] (2026-05-02) - Import Confirmation Popup, GIL Auto-Sync, Retail Scan Taint Fix
Improvements
- Single-class scans skip race subdivision entirely — when the active filter set narrows the class space to exactly one class (e.g. "Hunters only") and a single-level query hits the 50-cap,
RACEsplitat functions.lua:1901 now REPLACES the query with<level> c-<class>and doesn't subdivide by race. A single60-60 c-Hunter/who returns up to 50 Hunters across all races in one round-trip; race subdivision only kicks in if THAT class-tagged query also hits the cap, on whichRACEsplitis re-entered withqp.classalready set, the early branch is skipped, and the standard race-subdivide path runs with the class tag preserved. Per-race queries on the second pass are emitted in canonical<level> r-<Race> c-<Class>order sogetSearchDeepLvlcorrectly classifies them as sLevel=3 (the regex requiresr-beforec-); reconstructing from the level portion of the query also avoids the duplicate-tag trap that would have produced60-60 c-Class r-Race c-Class. HelpersoleAllowedClass(qmin, qmax)at functions.lua:1707 computes the forced class by intersecting the rejected-class sets of all active filters whose level range covers the query's level. Reads theclassFilterrejection flag as truthy rather than strictly== trueso any persisted value WoW treats as truthy is honored (covers historical save formats and sync from older peers).queryWorstCaseCostmirrors the optimization at sLevel=1 — credits1 (level) + 1 (class-only replacement) + N (compatible races)when the query has no class tag and1 (class-only) + N (compatible races)when re-entered with the class tag — so the progress bar denominator stays accurate. Races whoseRaceClassCombodoesn't include the forced class (e.g. Undead can't be Shaman on Classic Horde) are skipped on the second pass instead of queuing impossible combos. A[soleAllowedClass]debug-line print is gated behindaddon.debugfor/fgi debugdiagnosis. Only fires when class filtering is active and ends up at a single allowed class — no behaviour change for users with no class filter or a multi-class filter.
New Features
- GIL auto-sync on login — feature parity with the v1.9.3
grmAutoSyncopt-in. NewDB.global.ignoreAutoSyncflag (default false) added to defaults at FGI_Core.lua:489 and exposed via a new "Auto-sync GIL list on login" checkbox in Settings → Main, sitting between the GRM import button and the GIL import button. ThePLAYER_LOGINhandler at the bottom of functions.lua now defersfn:importIgnoreListBlacklist()by 2 s alongside the existing GRM auto-sync deferral; the same delay covers WoW's known login-time bug where every/ignoreentry comes back asUNKNOWNuntil the client finishes populating the cache (called out in the GIL readme), so the importer doesn't drop valid entries. New locale keysignoreAutoSyncandignoreAutoSyncTooltipadded toenUS.luaandruRU.lua. Persistence path mirrors GRM: checkboxOnClickwrites toDB.global.ignoreAutoSync, settings open re-reads the value at settings.lua:739.
Bug Fixes
- Retail: "secret string" taint spam from Scan.lua's invite-response handler —
pausePlayFilterat Scan.lua:109 registeredCHAT_MSG_SYSTEMon every client. On Retail the messages it cares about (decline / auto-decline / not-found / invite-sent) arrive as Blizzard "secret strings" — anystrfind/strsub/format/==against the message raises"attempt to perform string conversion on a secret string value (execution tainted by 'FastGuildInvite')", spammed the error log on every system message and tainted the addon's execution context for protected calls. Same fix shape as the v1.9.6 guard onfunctions.lua's guild-join handler: on Retail, registerUI_ERROR_MESSAGEonly —CHAT_MSG_SYSTEMis skipped entirely. UI_ERROR_MESSAGE delivers the decline / auto-decline / not-found message text without taint; the existing OnEvent dispatch already handled both events. Trade-off documented in the source: the "You have invited X to join your guild" success-side confirmation isn't an error and isn't delivered via UI_ERROR_MESSAGE, so the explicit invite-sent signal is dropped on Retail —pendingInvitesentries clear via the existing GUILD_ROSTER_UPDATE diff (when the player accepts) or via the decline / not-found UI_ERROR_MESSAGE branches (when they don't). - GRM importer prefix swapped to
<GRM>for consistency with<GIL>/<FGI>— for parity with the GIL fix above.fn:importGRMBlacklist()at functions.lua:977 now writes"<GRM>"(noreasonBanned) or"<GRM> <reasonBanned>"instead of the v1.9.3"GRM: <reason>"/"GRM: imported". SametryAddcontract as the GIL importer (added | refreshed | skipped) and the same legacy-reason refresh path: existing rows whose reason matches"GRM: ...","<GRM>", or"<GRM> ..."are upgraded in place on a re-import, so the user gets the new format on every old row by clicking Import GRM blacklist once after the upgrade. The legacy detector is GRM-specific so a GRM re-import won't touch GIL-imported rows and vice versa. Locale keygrmImportDoneupdated inenUS.luaandruRU.luato take three counts; the function's return signature is nowimported, refreshed, skipped(the GRM-not-loaded early return is also0, 0, 0). - GIL imports lost the GIL per-entry note and showed
"Ignored"instead —fn:importIgnoreListBlacklist()at functions.lua:1065 read two sources in this order: (1) the WoW built-in/ignorelist with the literal reason"Ignored", then (2)_G.GlobalIgnoreDBwith the better reason"GIL: <note>". Because the GIL addon mirrors every WoW/ignoreentry into its own list, every name was already present in the FGI blacklist by the time Source 2 ran, and the dedupe intryAddrejected the GIL upgrade — so the better note-bearing reason from GIL was thrown away and the blacklist held"Ignored"for everything. The reason format was also"GIL: <note>"instead of the angle-bracket prefix the addon uses everywhere else (<FGI>chat header, etc.). Three changes: (a) Source 2 (GIL with notes) now runs before Source 1 (WoW/ignorefallback), so each player's GIL note wins over the bare mirror entry. (b) The reason format is now<GIL>for no-note rows and<GIL> <note>when GIL carries a per-entry note. (c)tryAddreturns one ofadded | refreshed | skipped: when an existing blacklist entry's reason is one of the legacy auto-import strings ("Ignored","GIL","GIL: <note>","<GIL>","<GIL> <note>"), the row's reason is refreshed with the latest string from GIL instead of being skipped. So a single re-import after the upgrade rewrites every existing"Ignored"row to<GIL> <note>automatically — the user doesn't have to clear the blacklist and start over. Manually-typed reasons are detected via the legacy-prefix check and never overwritten. The import-done summary line and modal popup now read"GIL import: %d added, %d refreshed, %d skipped"(locale keysignoreImportDoneupdated in bothenUS.luaandruRU.lua); the function's return signature is nowimported, refreshed, skipped. - Scan
>>button on Retail and TBC un-greyed itself partway through every cooldown — companion to the click-rapidly fix below: the OnClick safety timer was scheduled atT = (DB.global.scanInterval or 5) + 2, but two facts make that wrong on Retail/TBC. (a)LibWho_Retail.luaoverrideslibWho:GetInterval()to return its own hardcodedretailConfig.interval = 8regardless of the user'sDB.global.scanInterval, so on Retail the safety timer fired at 7s while libWho's actual cooldown ran for ~10s. (b)timeCallbackStart(which setsscanCooldown=trueand starts the visible countdown label) doesn't fire when OnClick runs — it fires whenWHO_LIST_UPDATEarrives, 1-3+ s later. So the safety timer's countdown started from OnClick but libWho's countdown started fromWHO_LIST_UPDATE, leaving the safety timer firing several seconds before libWho'stimeCallbackEnd. Result: button visibly un-greyed and clickable for the last ~3-5 s of every cooldown while the timer label was still ticking down. TBC has the same shape of bug wheneverWHO_LIST_UPDATElatency is high enough; Classic generally has fast-enough/whoresponses that the windows happen to overlap. Fix: the OnClick safety timer at mainFrame.lua:721 now useslibWho:GetInterval() + 5instead ofDB.global.scanInterval + 2. The+ 5budgets for theWHO_LIST_UPDATEround-trip; usinglibWho:GetInterval()directly self-adjusts to whatever interval the active libWho variant returns (Classic/TBC: the user's configured scan interval; Retail: 8). The safety timer still does its job as a true backstop for stuck/whoqueries — it just can't fire ahead of libWho's normal cooldown end any more. - Scan
>>button on the main window looked clickable mid-cooldown when clicked rapidly — every click of the main window's>>button schedules aC_Timer.After(scanInterval + 2, ...)safety timer at mainFrame.lua:709 to force the cooldown off in casetimeCallbackEndfrom libWho never fires (stuck/whoquery). The timer's body wasif mainButtonsGRP.scanCooldown then setPausePlayCooldown(false) end— it had no way to tell which cooldown cycle it was the safety net for. When the user clicked >> again the moment the previous cooldown expired, click 1's safety timer was still pending its 2-second buffer; it fired ~2 s into click 2's cooldown, sawscanCooldown == true(correctly set by click 2'stimeCallbackStart), assumed it was its own stale cooldown, and turned the cooldown off — making the button visibly clickable for the rest of click 2's interval. Waiting longer thanscanInterval + 2between clicks hid the bug because click 1's safety timer had already fired and no-op'd before click 2 started its cooldown. The compact tray's>>button wasn't affected because it has no safety timer at all (it relies entirely on the libWhotimeCallbackStart/Endpair drivingcf.scanCooldown). Fix: a monotonicmainButtonsGRP.cooldownGencounter is incremented in OnClick after each new cooldown is set; the OnClick captures the new value intomyGenand the safety timer's callback only re-enables the cooldown whencooldownGen == myGenstill holds at fire time. Click 2's bump invalidates click 1's safety timer; click 2's own safety timer carriesmyGen = cooldownGenand continues to be the canonical safety net for click 2's cycle. Other code paths that toggle the cooldown (timeCallbackStart,timeCallbackEnd,clearSearch) deliberately leavecooldownGenalone so they don't accidentally invalidate a still-needed safety timer. - Window background opacity slider couldn't reach a fully-opaque window even at 1.0 —
fn:applyWindowOpacity()at functions.lua:927 only calledframe:SetBackdropColor(0, 0, 0, a)on each AceGUI window. The forked AceGUIFrameBackdropat Libs/GUI.lua:330 setsbgFile = "Interface\DialogFrame\UI-DialogBox-Background"— the standard WoW parchment texture, which carries its own per-pixel alpha channel.SetBackdropColormultiplies the requested alpha against the texture's existing alpha, so any pixel of the parchment that ships at alpha < 1 stayed translucent no matter what value the slider passed. The tooltip promised "1.0 = fully opaque" but in practice the user could still see the game scene through the window at the slider's max. Fix: lazily attach a solid blackBACKGROUND-drawlayer texture at sublevel -8 (drawn behind the backdrop's own bgFile) to each managed window the first timeapplyWindowOpacityruns, and scale its alpha with the slider alongsideSetBackdropColor. At slider = 1.0 the parchment's translucent pixels now show the solid black backing instead of the world behind, so the window is genuinely opaque; smaller slider values still let the world through proportionally because both layers fade together. Compact tray's existingcf.bgtexture wasn't affected by this bug (it was already a solid colour) and keeps its existing 0.7-multiplier behaviour. - GIL/GRM importers created
Name+Name-Realmduplicates in the blacklist — two layered issues, both surfaced by the v1.9.6 GIL importer. (1) The v1.9.3 connected-realm migration inFastGuildInvite:OnInitialize()(originally FGI_Core.lua:570) was gated byDB.global.migratedToFullRealmKeys.DB.globalis account-wide in AceDB, so once any character on any realm completed the migration, the flag was set and bare-name entries on every other realm in that account were never cleaned up — they sat there until a sync from another peer or an importer touched them. (2) The GIL importer'stryAddat functions.lua:1064 and the GRM importer at functions.lua:995 deduped using only the canonicalDB.realm.blackList[Name-Realm]lookup. With a stale bare-name entry likeBobstill in the table, the canonical lookup forBob-RealmAreturned nil and the importer added the player a second time asBob-RealmA, leaving the table holding both forms for the same player. Concrete reproducer: account with two characters across realms, the first character ran v1.9.3 migration on its own realm, second character on the other realm clicked Import GIL list — every same-realm player who was already on the FGI blacklist as a bare name got a duplicateName-Realmrow added with reason"GIL"/"Ignored". Fix: (a) the migration is now idempotent and ungated — runs on every login, walksDB.realm.blackListandDB.realm.blackListRemoved, and when a bare key has a corresponding canonical key on the local realm the bare key is dropped (canonical wins, since it's the form everything else writes today); when there's no canonical sibling, the bare key is renamed to canonical (the original v1.9.3 behaviour). The flag is still set true for any external code that may still read it. (b) Both importers now dedupe viaIsInBlackList(name)(which checks canonical, then bare, then prefix) instead ofDB.realm.blackList[fullName], so even if a bare-name entry slips in mid-session via sync the importer won't double-add. (c) Tombstone checks intryAddalso look up the bare-name form so a player tombstoned pre-v1.9.3 stays tombstoned through a Name-Realm import attempt. Existing duplicate rows from before the upgrade are auto-cleaned at next login by the new normalize pass — no user action required. - GIL / GRM import buttons gave no visible confirmation that the import ran — both
fn:importIgnoreListBlacklist()(GIL + WoW/ignore) at functions.lua:1040 andfn:importGRMBlacklist()at functions.lua:931 only emitted aprint("<FGI> ...")line to the default chat frame after computing theimported / skippedcounts. Users who had busy chat windows, channel-filtered tabs, or simply weren't watching chat at the moment the button was clicked saw nothing happen and assumed the button was broken — particularly likely with the GIL importer since fresh installs often have an empty/ignorelist and a missingGlobalIgnoreDB, so a0 added, 0 skippedchat line is even easier to miss. Fix: both importers now also calladdon.API.ShowMessage(msg)after the chat print, surfacing the same"GIL import: %d added, %d skipped..."/"GRM import: %d added, %d skipped..."text in a modalmessage()popup on Classic / Anniversary / TBC / Wrath / Cata and viaUIErrorsFrame:AddMessageon Retail (theaddon.API.ShowMessagewrapper at FGI_APICompat.lua:229 already handles the version split because Blizzard removedmessage()from the Retail global namespace). Chat print is preserved so the result still lives in chat scrollback for users who want it.
[v1.9.6] (2026-04-30) - GIL Import, Retail Joins, Filter-Aware Progress
New Features
- Import GlobalIgnoreList (GIL) and WoW
/ignorelist into the blacklist — new "Import GIL list" button in Settings → Main mirrors the v1.9.3 "Import GRM blacklist" importer (label uses "list" not "blacklist" because GIL stores an ignore list, not bans). Reads two sources, both optional: (1) the WoW built-in/ignorelist viaC_FriendList.GetNumIgnores()/C_FriendList.GetIgnoreName()(always available, capped at 50), and (2) theGlobalIgnoreListaddon's account-wide ignore list via_G.GlobalIgnoreDB.ignoreList(no 50-cap, syncs across characters, supports per-entry notes). Implementation infn:importIgnoreListBlacklist()at functions.lua:1021; reads GIL's parallel-array layout (ignoreList[i]/typeList[i]/notes[i]) and only importstypeList == "player"entries, skipping NPCs and server-wide ignores. Reason text is"Ignored"for the WoW list and"GIL: <note>"for GIL entries (or just"GIL"when no note). Same realm-set filtering as the GRM importer (only same-realm + connected realms viaGetAutoCompleteRealms) and same tombstone respect (DB.realm.blackListRemovedskips entries the user previously removed from FGI). Locale stringsignoreImportButton,ignoreImportButtonTooltip,ignoreImportDoneadded inenUS.luaandruRU.lua. Button positioned in Settings between the existing GRM auto-sync and the "Show update info" toggle.
Bug Fixes
- First-cycle progress bar denominator was the unfiltered worst case, never reaching 100% before cycle wrap —
queryWorstCaseCost(query)at functions.lua:1508 computed the per-level cost as1 + |L.race| + Σ|RaceClassCombo[race]|, treating every race and every class as if it would actually be scanned. But the live scan skips: (a) races on the filter'sraceFilterignore list, (b) classes on the filter'sclassFilterignore list, (c) races whose only classes are all on the class-ignore list (the v1.9.5 race-class compatibility skip at functions.lua:1354). With a "Shaman/Hunter only" class filter on Classic Era a 10-level range estimated ~500 scans but actually ran ~150 — the bar climbed to maybe 30 % then wrapped to a corrected denominator on cycle 2 (the existing wrap-recompute path at functions.lua:1872 does use the post-subdivision queue, which is why cycle 2+ looked fine). Fix:queryWorstCaseCostnow reusesisQueryFilteredto predict whether each candidate race/class subquery will survive the filter pass and only counts the ones that will. The level portion of the parent query is preserved when constructing the sample subqueries so the filter's level-bounds check sees realistic values. Filters-off path is gated behind a quicknext(DB.realm.filtersList) ~= nilcheck so users without active filters skip the per-raceisQueryFilteredcalls entirely (no behaviour change for the unfiltered case). - Scan
>>button still firednextSearch()during the cooldown — the v1.9.3-v1.9.5 fixes greyed the button visually and aligned the safety timer with the configured scan interval, but the OnClick handler at mainFrame.lua:686 had no explicit cooldown guard. It relied entirely onButton:Disable()to suppress clicks. Two paths bypassed that: the keybind handler at FGI_Core.lua:652 callspausePlay.frame:Click()directly, and per WoW's API:Click()doesn't honour the disabled state — OnClick fires anyway andnextSearch()runs. On modern retail (11.x), custom OnClick scripts set viaframe:SetScript("OnClick", ...)on a UIPanelButtonTemplate button are also not always gated byDisable()the way the template's built-in click path is, so even direct mouse clicks could land mid-cooldown on retail / Anniversary. Fix: added an explicitmainButtonsGRP.scanCooldownflag (set/cleared bysetPausePlayCooldown(on)alongsideframe:SetDisabled(on)) and check it at the top of OnClick. The flag is the same pattern the compact tray button at compactFrame.lua:113 already uses (cf.scanCooldown), so both views now reject cooldown clicks the same way regardless of how the click is dispatched. Safety timer at the bottom of OnClick now also reads the flag instead of:IsEnabled(), so its re-enable check stays in sync with the explicit state we're tracking. - Accepted counter and auto-welcome both broken on retail when someone joined the guild — the
CHAT_MSG_SYSTEM"X has joined the guild" handler at functions.lua:590 (Accepted counter / accept history) and the matching one at FGI_Core.lua:155 (auto-welcome guild-chat message + auto-welcome whisper) both short-circuit on retail at line 170 becausemsgis a tainted secret string —msg:matchraises taint errors per system message and would spam the error log. The handler comment already noted "Guild-join detection on Retail would need to be reimplemented via GUILD_ROSTER_UPDATE diffing; skipping here keeps the addon quiet" — that work was deferred and both features just no-op'd on retail. Fix: added a retail-onlyGUILD_ROSTER_UPDATEhandler that snapshots the guild roster (keyed by normalised name to matchpendingInvitesandalreadySended) and diffs against the previous snapshot. Each new member bumpsfn.history:joined(); members inpendingInvitesoralreadySendedadditionally bump the Accepted counter and log an "accepted" history entry; and the same loop fires the auto-welcome guild-chat message and welcome whisper if the user has them configured in the Guild settings tab. The chat-templateNAMEsubstitution usesAmbiguate(name, "none")so the message reads cleanly without a-RealmNamesuffix; the whisper target keeps the realm-suffixed name so cross-realm whispers route correctly. 2.5 s delay before sending matches the Classic path. Gated on member-count growth (n <= snapshotSizeshort-circuits) so the full roster scan only happens when there's actually a new member, not on every status-change fire (which retail does dozens of times per minute on busy guilds). Classic / TBC / Wrath / Cata still use the existingCHAT_MSG_SYSTEMpaths — no behaviour change there.
[v1.9.5] (2026-04-29) - Scan, Filter, & UI Polish
Improvements
- Shaman class colour is now version-specific — Classic Era and Anniversary use the original vanilla convention where Shaman shares Paladin's pink (
#F58CBA), since the two are faction-mirror classes (Shaman = Horde-only, Paladin = Alliance-only) on those versions. Retail / TBC / Wrath / Cata continue to use Blizzard's canonical blue (#0070DE) fromRAID_CLASS_COLORS, since both factions can roll either class on those versions and visually distinct colours match what players see in the game's own UI. Resolved at addon load by gating onaddon.gameVersion.isClassicat init.lua:54. Required swapping the TOC load order soFGI_Compatibility.lualoads beforeinit.luain all 5 TOC files — previously init.lua ran first and sawaddon.gameVersionas nil, so the version check evaluated to false and Classic always fell through to blue. The comment in init.lua already said "FGI_Compatibility.lua must be loaded first", but the actual TOC ordering didn't match that intent. - Compact tray's
>>scan button now shows the cooldown countdown — the compact tray's scan button at compactFrame.lua:103 was a plain text button that always read>>, with no visual indication of the WHO-throttle interval. Users in compact mode had to guess when the scan was ready and click repeatedly hoping it'd take. The main window's>>button has had a numeric countdown viapausePlayLabelsince v1.9.0, driven bytimeCallbackStart/ ticker /timeCallbackEndfrom libWho. Addedcf.setScanCooldown(remaining)to compactFrame.lua that swaps the label between>>and the seconds remaining, dims the label colour, and hides the hover highlight while counting down. The compact button's OnClick short-circuits viacf.scanCooldownwhile active. Hooked the libWho callbacks in functions.lua so the same chain that drives the main window's countdown also updates the compact tray button — both views stay in sync, and the compact OnClick also applies an immediate cooldown so rapid clicks don't fire extra/whorequests in the gap before WHO_LIST_UPDATE arrives. - Race subdivision now respects the class filter via
RaceClassCombo—isQueryFilteredat functions.lua:1306 only consulted the explicit race-ignore list at sLevel=2 (level + race query). If a filter restricted classes (e.g. "wanted Shaman/Hunter only", with all other classes on the ignore list) but had no race exclusions, every race got subdivided and queried even when the race couldn't be any non-excluded class. The race query came back, every result got individually filtered out, then the queue subdivided each useless race by class, where the class-level check finally caught it — wasted time and inflated the progress denominator. Added a race-class compatibility check at sLevel=2: look up the query's race inRaceClassCombo, and if every class the race can be is on the filter's ignore list, mark the race query as filtered. Concrete impact: on Classic Era with "wanted Shaman/Hunter only", the addon now skips Undead, Gnome, and Human entirely (none of those races can be either class). On retail the gain is filter-dependent — every race in the retailRaceClassCombocan be Hunter, so a Hunter-inclusive filter doesn't shrink the race set, but tighter filters (e.g. "Druid only", "Demon Hunter only", "Shaman only") drop most races at the race-subdivision step instead of querying-then-filtering each one.
Bug Fixes
- "Found 50, subdividing for full coverage..." printed even when no further subdivision was possible —
willSubdivideat functions.lua:1750 was set to true wheneversearchLvl ∈ {1,2,3}and#results >= 50, but the dispatch table insidesearchAddWhoList(line 1547) only subdivides at sLevel=3 when#DB.factionrealm.locations > 0. Most users have no saved locations, so a level-3 query (race + class) returning 50 results printed the subdivide message and then quietly skipped every subdivision branch — no new queries got queued. The mismatch also shorted the progress denominator:cost = willSubdivide and 1 or queryWorstCaseCost(query)credited 1 instead of the full worst-case, making the bar drag for the rest of the cycle. Fix: gate the sLevel=3 case on#DB.factionrealm.locations > 0to match the actual dispatch — the message only fires when subdivision will really happen, and the cost calculation now correctly credits the full worst-case for queries that won't subdivide. - Scan
>>button became yellow/clickable before the visible cooldown timer reached zero — the safety re-enable timer in the OnClick handler at mainFrame.lua:680 usedFGI_DEFAULT_SEARCHINTERVAL + 2(= 7 s, hardcoded constant from before the v1.9.x configurable scan-interval feature). The user-facing scan interval (DB.global.scanInterval, 2-30 s, configured in Settings > Main) drives the visible countdown vialibWho:GetInterval(), andtimeCallbackEndre-enables the button after that interval. When the user set the interval above 5 s (e.g. 10, 15, 30), the safety timer fired first at 7 s while the visible countdown still had time left, callingsetPausePlayCooldown(false)which restored the highlight texture — making the button look hover-clickable mid-cooldown. The v1.9.4 fix made highlight show/hide reliable, but didn't address the safety timer firing early. Fix: the safety timer now reads(DB.global.scanInterval or FGI_SCANINTERVALTIME) + 2at click time, so it always outlasts the configured scan interval regardless of the user's setting. The +2 buffer keeps the safety net intact for the rare case wheretimeCallbackEnddoesn't fire (WHO query never resolves). - "Next scan" label briefly blanked to "—" at the end of every scan cycle before the loop wrapped —
scanInfo.update()at mainFrame.lua:395 computednextQueryfromwhoQueryList[progress](with the v1.9.4 between-fire-and-callback offset), but didn't account for the cycle-wrap window. After the WHO callback for the last entry in the queue fires,progressadvances past#whoQueryList; both branches of the if/else then resolve tonilbecause the index is out of bounds. The label rendered "Next scan: —" until the very nextnextSearch()call clampedprogressback to 1 (functions.lua:1884) and re-firedwhoQueryList[1]. Most visible to retail users, where shorter scan intervals make the wrap window land more often. The loop is genuinely cyclic andnextSearch()will firelist[1]next, so showing it eagerly matches reality. Fix: when the computednextQueryis nil and the queue is non-empty, fall through tolist[1]. - Anti-Spam List "Clear All" confirmation popup didn't go away after clicking Yes — the
FGI_ANTISPAM_CLEAR_ALLStaticPopupDialog'sOnAcceptat antiSpamList.lua:211 ended withreturn true. Blizzard'sStaticPopup_OnClickinterprets the OnAccept return value as a "keep the popup open" flag (hide = not OnAccept(...)), so returningtrueleft the confirmation dialog on screen after Yes was clicked — making it look like nothing happened. Other dialogs in the codebase (FGI_BLACKLIST,FGI_BLACKLIST_CHANGE) also returntrue, but they explicitly callStaticPopup_Hide(...)first to chain to a follow-up popup; this one didn't have that chain, so the dialog just sat there. Fix: removedreturn true, so the popup auto-hides after OnAccept runs. While there, switched the data clear fromDB.realm.alreadySended = {}(whole-table reassignment) to an iterate-and-nil loop that preserves the AceDB-managed table reference, and also flushedaddon.search.tempSendedInvitesso the in-session scan cache doesn't keep filtering players the user just cleared from the persistent list.
[v1.9.4] (2026-04-28) - Scan & Progress Bar Fixes, Vertical-Only Main Window
Bug Fixes
- Scan
>>button cooldown helper threwbad argument #2 to '?'— the v1.9.3setPausePlayCooldown(on)helper on the>>button tried to clear/restore the highlight texture withframe:SetHighlightTexture(nil)(clear) andframe:SetHighlightTexture(textureObj)(restore). The WoW API rejects both:SetHighlightTexturerequires a string asset path, so passingnilraises"bad argument #2 to '?' (Usage: self:SetHighlightTexture(asset [, blendMode]))"and passing the Texture object never reaches the restore path because the disable call already errored. Fix: hide/show the existing texture object directly viapausePlayHighlight:Hide()/:Show(). The captured texture reference at construction time stays valid for the lifetime of the button. - Progress bar started at 70% on login — the
SetProgress(self, cur)function at the top of mainFrame.lua:108 had acur = (cur or self._lastCur or 70)fallback. At login, the bar is initialised by a:SetProgress()call with no argument before any scan has run, socur == niland_lastCur == nilfalls all the way through to70and renders the bar at 70%. Changed the fallback to0. - "Next scan" label duplicated "Last scan" between fire and callback —
scanInfo.update()readwhoQueryList[progress]for the next-scan slot, butnextSearch()callsupdate()immediately after settinglastQuery = curQueryand BEFORE the WHO callback incrementsprogress. So during the entire scan-interval window (5+ seconds) Next showed the same query as Last. Especially visible with narrow filters (e.g. only Shamans at level 70) where each query takes the full interval. Fix:update()now detects the between-fire-and-callback state by checkinglist[progress] == lastQuery, and looks one slot ahead in that case. - Scan
>>button could end up perma-disabled or with missing highlight — there are two paths that disable/re-enable the scan button: the OnClick path (uses the v1.9.3setPausePlayCooldownhelper that hides/shows the highlight texture) and the LibWho callbacks (timeCallbackStart/timeCallbackEndin functions.lua, which used rawpp:SetDisabled(...)). Whichever fired first won; the other path's state could be left out of sync. When the LibWho timer re-enabled the button before the OnClick safety timer fired, the OnClick safety timer saw the button as enabled and skipped the highlight restore — highlight stayed hidden permanently and the button looked broken/stuck untilclearSearch()(Reset) ran. Fix:timeCallbackStart/timeCallbackEndnow route throughmainButtonsGRP.setPausePlayCooldown(with a fallback to rawSetDisabledif the main frame isn't constructed yet), so both disable paths and both re-enable paths leave the button in a single consistent state. - Settings Main scrollbar didn't respond to the mouse wheel — the v1.9.3 scroll wrapper had
EnableMouseWheel(true)and anOnMouseWheelhandler on theScrollFrame, but mouse wheel events in WoW don't bubble to parents; they go to the topmost frame at the cursor withEnableMouseWheel(true). The AceGUI checkboxes inside the Main page haveEnableMouse(true)(for click handling) but no wheel handler, so they were "topmost" at the cursor and silently swallowed wheel events instead of letting them reach the ScrollFrame underneath. Fix: added a transparent overlay frame covering the entire scroll viewport withEnableMouse(false)(clicks still pass through to the checkboxes) andEnableMouseWheel(true)(wheel events land on the catcher and forward to the scrollbar). The catcher sits at frame level +100 so it's reliably above the AceGUI children regardless of their internal frame ordering. - Progress bar shot past 100% on level-range scans — the v1.9.3
queryWorstCaseCost(query)only handled single-level queries: it returned1 + |races| + sum_classes(~25 on Classic Era Horde) regardless of how wide the level range was. But level ranges subdivide BY LEVEL FIRST (binary split) before race/class subdivisions, so a70-80query is really11 single-level slices × 25 + 10 internal binary-split nodes = 285worst-case scans, not 25. Result: as soon as one /who in a range scan returned <50, it credited the (under-counted) 25, blowing past the denominator and showing things like52/25(208%). Fix: range-aware formularange_size × per_level_cost + (range_size - 1). Single-level queries still return the same value as before; only ranges change. - Progress bar stacked across scan cycles instead of resetting at 100% — the addon scans cyclically: once the queue is consumed,
nextSearch()wrapsprogressback to 1 and re-fires every query so newly-online players get caught. v1.9.3 setprogressTotalonce at the first scan and letprogressDonekeep accumulating across cycle wraps, producing values like1559/1559 → 2338/1559after the second pass. Fix:nextSearch()now detects the wrap (progress > #queue) and recomputesprogressTotalfrom the CURRENT queue (which may have grown via subdivisions during cycle 1) plus resetsprogressDoneto 0. Each full pass through the expanded queue now shows a clean 0→100% sweep.
Improvements
- Main window is now vertical-only resizable — same pattern as the v1.9.2 history window. Width is locked at
MAIN_W = 635(bumped from the locale default of 620 — see button-row centering note below), height stays free between 450 and 1500. The right-edgesizer_egrip is hidden because horizontal drag has no effect, but the bottom-rightsizer_secorner grip stays visible (its diagonal-line texture is the universal "this window is resizable" affordance) —StartSizing("BOTTOMRIGHT")only changes height because the width bounds are clamped. The PLAYER_LOGIN restore stops applying any saved custom width; users with a non-default saved width will see a one-time snap toMAIN_W. Height is still saved/restored across sessions. - Bottom button row clipped against the window border — the locale default
size.mainFrameW = 620leftmainButtonsGRP(width =MAIN_W - 20 = 600) too narrow for the 605 px button row (8 buttons: 3×57 + 4×95 + 1×40 + 7×2 gaps), so the leftmost (Invite) and rightmost (Compact toggle) buttons each overflowed ~2.5 px pastmainButtonsGRPand clipped against the window's inner border. BumpedMAIN_Wto 635 —mainButtonsGRPbecomes 615 px wide, leaving 5 px of clean padding either side of the 605 px row. Centering anchor for the Invite button shifted from -273 to -274 to keep the row exactly symmetric. - Settings Main: wheel dead zone between widgets and scrollbar — the v1.9.4 wheel catcher used
:SetAllPoints(settings.mainScroll)so it only covered the ScrollFrame's footprint. The scrollbar lives anchored to the settings WINDOW's right edge (not the ScrollFrame's), so the strip between the ScrollFrame's right edge and the scrollbar's left edge had no wheel-enabled frame and silently dropped wheel events. Fix: anchor the catcher frommainScrollTOPLEFT tomainScrollBarBOTTOMRIGHT so it spans the entire content area through the scrollbar. The catcher still hasEnableMouse(false)so clicks on the scrollbar slider still work. - History retention and scan interval input boxes were oversized for 3-digit values — both EditBoxes (history retention bounded 0-365, scan interval bounded 2-FGI_SEARCHINTERVAL_MAX) were sized to fill ~180-220 px, way more than a 3-digit number needs. AceGUI's EditBox layout has both
label(TOPLEFT→TOPRIGHT) andeditbox(BOTTOMLEFT→BOTTOMRIGHT) anchored to fill the widget frame, so a naiveSetWidth(60)would shrink the label too. Fix on both: keep the widget frame at its original width so the label renders fully, then narrow only the inner editbox by replacing its right anchor with a fixedSetWidth(50)(fits "365" with a few px breathing room).
[v1.9.3] (2026-04-27) - Connected-Realm Blacklist, GRM Import, Smarter Filters, Opacity, Progress
New Features
- feat: GRM blacklist import — new Settings page Button (
Import GRM blacklist) reads active bans from Guild Roster Manager and adds them to the FGI blacklist, prefixed withGRM:so the source is visible in the blacklist UI. Walks bothGRM_GuildMemberHistory_SaveandGRM_PlayersThatLeftHistory_Save, importing any entry wherebannedInfo[1] == true and bannedInfo[3] == false. Filters by the connected-realm set (GetAutoCompleteRealms()+ current realm) so cross-cluster bans aren't pulled in. Skips entries already in the FGI blacklist and respectsDB.realm.blackListRemovedtombstones so re-imports don't resurrect entries the user deleted from FGI. ReportsN added, M skippedafter each run. - feat: GRM blacklist auto-sync — opt-in Settings checkbox
Auto-sync GRM blacklist on login(default off) runs the importer 2 seconds after every PLAYER_LOGIN; the delay covers GRM's own SavedVariables load timing. - feat: Window background opacity slider — new Settings slider
Window background opacity(range 0.3–1.0, default 1.0) controls the background transparency of the main window, Settings, Statistics, History, and the compact tray simultaneously. Single-passfn:applyWindowOpacity()walksinterface.{mainFrame,settings,graphFrame,historyFrame,compactFrame}and updates the AceGUI:SetBackdropColor(or for the compact tray's plain texture,cf.bg:SetColorTexturescaled against the existing 0.7 base). Applied on slider drag and once on PLAYER_LOGIN; sticky across hide/show. - feat: Worst-case progress denominator — the main window progress bar now never moves backward when a /who returns 50 results and gets subdivided. At scan start,
fn:queryWorstCaseCost(query)precomputes the maximum number of /who calls each level slice could fan out into (1 + |races| + sum_over_races(|classes_for_race|)), and the bar fills based onactualCompleted / worstCaseTotal. A query that doesn't subdivide credits its full worst-case (skipped subdivisions "cover" themselves); a query that does subdivide credits only itself, then its children credit their own worst-case as they complete. Net result: the bar fills smoothly, hits 100% early when filters or low population skip subdivisions, and never reverses. - feat: Subdivision chat note — when /who returns 50 results and the addon queues subdivisions, a single chat line
<FGI> Found 50, subdividing for full coverage...prints once per breakdown event. Suppressed by the existingaddonMSGmute setting. Helps users understand why the progress bar isn't already at 100%. - feat: Level spinner push behaviour — scrolling the min-level spinner upward past the max (or the max spinner downward past the min) now pushes the partner spinner along instead of clamping silently. New
widget:SetPushPartner(cb)API onmakeLvlSpinner; the callbacks bumpDB.global.{low,high}Limitby the same scroll mod (1 normal, 5 with Shift) and call:SetTexton the partner widget so the displayed value follows the DB state. - feat: Officer-chat blacklist messages tagged
<FGI>— both add and remove notices in the officer channel now begin with the<FGI>prefix so they're trivially distinguishable from messages emitted by Guild Roster Manager and other addons that broadcast their own blacklist activity. - feat: Settings page Main scroll — the Main page now wraps its widget group in a
CreateFrame("ScrollFrame")+UIPanelScrollBarTemplateslider (mirroring AceGUI's own ScrollFrame pattern). The settings window stays non-resizable; the inner GroupFrame is sized 800 px tall so every widget fits with room for future additions, and the scroll wrapper clips/scrolls. The scrollbar is anchored to the settings window's right edge (not the ScrollFrame's right edge) so it sits flush against the window border instead of floating mid-window over content. Mouse wheel scrolls 30 px per tick. - feat: Compact tray invite button — added a
+(N)invite button to the title row of the compact tray, positioned just to the left of the>>scan button. Click invites the head of the queue viafn:invitePlayer()(same as the main UI's invite button); the(N)count refreshes viacf.refresh()so it always tracks#addon.search.inviteList, mirroring the main window'sinviteBtnText()behaviour. - feat: Tooltip auto-flip helper — new
addon.Tooltip.Owner(frame)in functions.lua (mirrored fromTOGProfessionMaster'saddon.Tooltip.Owner) replaces every rawGameTooltip:SetOwner(frame, "ANCHOR_TOP")call in FGI. The helper anchorsANCHOR_BOTTOMLEFTwhen the frame is in the top half of the screen,ANCHOR_TOPLEFTwhen in the bottom half — so tooltips on top-row buttons (compact tray, main window button row) appear BELOW instead of above-and-centered over the cursor. Routed through 13 FGI sites plus 4 widget tooltip handlers in Libs/GUI.lua, so every AceGUIwidget:SetTooltip()(every checkbox, slider, button in Settings) now auto-flips too.
Bug Fixes
- Blacklist entries collided across connected realms (data corruption) — the previous
fn:blackList()called a now-deletedremoveSelfRealm()helper that stripped the-MyRealmsuffix from any name on the player's own realm, so on a connected-realm clusterbob-RealmAandbob-RealmBcollapsed to the same keyboband overwrote each other's reasons. Same problem infn:unblacklist,fn:isInBlackList, the lookup variants inIsInBlackList,fn:setNote, the whisper bookkeeping infn:sendWhisper/fn.hideWhisper, and Sync'sIsTrustedPlayer. Fix: deletedremoveSelfRealm(); addedfn:fullPlayerName(name)which always returns canonical Name-Realm regardless of client; rewired every storage and lookup site to use canonical full names. The Retail-only API call boundary (C_GuildInfo.InviteandC_ChatInfo.SendChatMessageboth fireERR_*_PLAYER_NOT_FOUND_Swhen givenName-MyRealm) now goes through a newfn:formatNameForRetailAPI(name)helper that strips the suffix only at the API call, leaving storage canonical. One-shot DB migration inOnInitializewalksDB.realm.blackListandDB.realm.blackListRemoved, appending the current realm to any bare-name key; gated byDB.global.migratedToFullRealmKeysso it runs only once. - Filter race/class exclusions didn't prune the /who queue —
isQueryFilteredhad a structural bug in thesLevel == 2andsLevel == 3branches where the race-exclusion check requiredf.class == nil, so a filter that excluded both Undead AND Druid would still let an Undead-tagged /who through (wasting the scan budget). Rewrote the function so race-exclusion and class-exclusion are independent dimensions: anr-Undeadquery is filtered if Undead is on the ignore list, regardless of which classes are also restricted. Same fix applied at sLevel 3 and 4 (race+class+zone). Level-only queries unchanged — they're only filtered when the filter has no race/class exclusions, since a level scan still has to run to find the included races/classes. - Filter "enabled" status reset to off every time the user edited the filter — the save path at the bottom of
addfilterFrame.saveFilter()hardcodedfilterOn = falsefor both new filters and edits. Fix: detect the edit case by checking whetherDB.realm.filtersList[filterName]already exists, and preserve the existingfilterOnandfilteredCountinstead of clobbering them. - Scan
>>button looked clickable during the 5-second cooldown — AceGUI's Button usesUIPanelButtonTemplate; calling:SetDisabled(true)correctly disables clicks but the amber highlight texture still renders on mouseover, which fooled users into thinking they could press the button mid-cooldown. Fix: introduced asetPausePlayCooldown(on)helper that calls:SetDisabled(true)and also strips the highlight texture (SetHighlightTexture(nil)) for the duration; the original highlight is captured at construction time and restored on re-enable.clearSearch()routes through the same helper. - Retail CHAT_MSG_SYSTEM secret-string taint spam (FGI_Core copy) — v1.9.0 fixed this in functions.lua but missed the duplicate
CHAT_MSG_SYSTEMhandler in FGI_Core.lua:157, which still ranstrfind/strsub/msg:matchon every system message. On Retail 11.x those payloads are tagged "secret strings", and any string operation raises"attempt to perform string conversion on a secret string value (execution tainted by 'FastGuildInvite')"— once per system message, hundreds per session. Added the samenot gv.isRetailearly-return guard. Leave detection on Retail is already covered byprocessGuildEventLogviaC_GuildInfo.GetGuildEventLog; auto-welcome on Retail would needGUILD_ROSTER_UPDATEdiffing (not implemented here, was already non-functional on Retail due to the taint).
Internal
fn:fullPlayerName(name)— sibling of the existingfn:normalizePlayerName. WherenormalizePlayerNameonly canonicalizes on Retail (the documented existing behaviour),fullPlayerNamealways returns Name-Realm regardless of client. Used by the connected-realm fix and the GRM importer; not retroactively applied toalreadySended/leave(still keyed vianormalizePlayerName) — those tables are session-ish and a wider migration is out of scope for this patch (noted indocs/DEV_NOTES.md).fn:formatNameForRetailAPI(name)— pure no-op on non-Retail; on Retail strips only the own-realm suffix and leaves cross-realm names alone. Used byfn.invite(FGI_APICompat.lua) andfn:sendWhisper(functions.lua) to work around the Retail API bugs that made the realm-stripping necessary in the first place.fn:queryWorstCaseCost(query)— returns the maximum /who-call cost a query could fan out into.1for sLevel ≥ 3;1 + |classes_for_race|for sLevel 2;1 + |races| + sum_over_races(|classes|)for sLevel 1. Used by item C to fix the progress denominator at scan start.fn:applyWindowOpacity()— appliesDB.global.windowOpacityto all five FGI windows. Called on slider change (Settings) and once on PLAYER_LOGIN. Compact tray's plain texture (cf.bg) is now exposed on the frame so the helper can rescale it.fn:importGRMBlacklist()— new public API; returnsimported, skippedso external integrations could call it programmatically. Bails early with a localised "GRM not loaded" print if neither GRM SavedVariable is present.- DB schema additions:
DB.global.windowOpacity(default 1.0),DB.global.grmAutoSync(default false),DB.global.migratedToFullRealmKeys(one-shot migration flag). - PLAYER_LOGIN handler in functions.lua — extended to also call
fn:applyWindowOpacity()and (when enabled) schedule the deferred GRM auto-sync viaC_Timer.After(2, ...). - Locale keys added —
scanSubdivide,windowOpacity,windowOpacityTooltip,grmImportButton,grmImportButtonTooltip,grmAutoSync,grmAutoSyncTooltip,grmNotLoaded,grmImportDone— added to bothenUS.luaandruRU.lua.
[v1.9.2] (2026-04-27) - Reset Fixes, Compact Tray, Invite History, Popup Toggles
New Features
- feat: Compact tray window — a separate slim Track-o-Matic-style overlay (new file compactFrame.lua) replaces the main window when
DB.global.compactModeis enabled. 18-px title row with FGI tag, live counters (F:N S:N A:N X:N), a>>Pause/Play button matching the main window, and a+expand-back icon. 5 queue rows beneath the title row each with the same Invite / Skip / Decline / Blacklist icons as the full window. Mouse-wheel scrolls the queue, frame auto-resizes to the visible row count, position persisted inDB.global.compactFrame, registered asFGICompactFrameinUISpecialFramesso ESC closes it. Toggle via the new minus-icon button on the full main window's bottom row, the Settings checkbox, or/fgi compact(also a diagnostic). - feat: Invite History page — new
Historybutton next toStatisticsopensinterface.historyFrame, a scrollable per-invite log showing time / name / level / class / outcome (Accepted, Declined, Anti-spam, Blacklisted). Newest entries first. Outcomes are coloured by category. Empty-state and entry-count footer included. Vertical-only resize between 250 and 1500 px tall — width is locked at 660 to keep columns from clipping. Visible row count is computed live from viewport height so growing the window taller actually surfaces more rows. Bottom-right corner grip stays visible as a "this window is resizable" affordance even though horizontal drag is locked. - feat: Configurable history retention — new
Invite history retention (days)setting (range 0–365, 0 = forever) controls how long invite-history entries are kept. Default 30.fn.history:trim()runs on everylogInvite()call and once on PLAYER_LOGIN; entries are appended in time order so trim is a single linear scan that drops the leading prefix. Saved inDB.global.historyRetentionDays; data lives inDB.factionrealm.history.invitesto match the existingfn.historyscope. - feat: Per-row Blacklist button — fourth icon button on each player row (red X texture from
Interface\Buttons\UI-GroupLoot-Pass-Up) immediately blacklists the player and removes the row from the queue, replacing the right-click → Black List menu round-trip. Wired throughfn:blackList(name)andfn.history:logInvite("blacklisted", ...)so the action shows up on the new History page. - feat: Last/next scan parameters display — new
mainFrame.scanInfolabel below the counters row shows the most-recently issued WHO query (Last scan: 10-19) and the next one queued (Next scan: ...), updating on every list refresh and at eachnextSearch().addon.search.lastQueryis the new field that records the just-issued query. - feat: Settings / Statistics / History buttons toggle their popups — clicking each button opens the popup if closed and closes it if open (was open-only). None of the three popups now hide the main window when opening — they're independent overlays so you can keep the main UI visible while you check stats / history / settings.
- feat: Diagnostic slash command
/fgi compact— dumps the compact frame's runtime state (visibility, strata, scale, bounds, anchor, screen size) and force-shows it at screen centre. Useful if the tray ever ends up in an unreachable position.
Bug Fixes
- Compact tray invisible / silently failing on Retail, Anniversary, TBC, Wrath, Cata — the per-expansion TOC files (
FastGuildInvite_Mainline.toc,_BCC.toc,_Cata.toc,_Wrath.toc) never listedcompactFrame.luaorinviteHistory.lua; only the baseFastGuildInvite.toc(Classic Era) loaded them. Sointerface.compactFramewas nil on every other client andapplyCompactModehad nothing to show. Added both files to all four per-expansion TOCs. - Compact tray buried behind world UI on Retail — pure-Lua frames need
SetToplevel(true)set explicitly to be brought to the top of their strata when shown; XML-loaded frames (like Track-o-Matic's) get this from thetoplevel="true"attribute. Without it, Retail's UI manager left the tray at the back of the HIGH strata. AddedSetToplevel(true)andSetFrameLevel(200)so the bar reliably surfaces above other addons sharing the same strata. - Compact tray rendering off-screen after a reload —
cfhad noSetPointuntil PLAYER_LOGIN. IfShow()ran before the position-restore (or with a stale saved offset from a different UI scale / monitor), the frame either rendered at WoW's unanchored default (bottom-left 0,0) or at an unreachable saved position. Fixed by setting a defaultSetPoint("CENTER", UIParent)immediately at file load, validating savedxOfs/yOfsin PLAYER_LOGIN (anything beyond ±3000 px is discarded as stale), and addingcompactFrameto/fgi resetWindowsPosfor emergency recovery. - Per-row icon buttons overlapped the player-list scrollbar — Invite/Skip/Decline/Blacklist were anchored from
listBG.TOPLEFTwith absolute X offsets, so at the default window width the rightmost (Blacklist) icon clipped under the 12-px scrollbar gutter. Re-anchored all four icons fromlistBG.TOPRIGHTwith negative offsets and a 16-px scrollbar-clearance gutter, so they always stay clear regardless of window width. - Bottom button row clipped at the default window width — adding the History and Compact-toggle buttons widened the row to ~605 px, which exceeded the previous 580-px window minimum. Bumped the minimum (
SetResizeBounds) to 640 px so the row never gets clipped; old saves clamped upward on load. - History tray's outcome column clipped under the scrollbar — the column layout (Time/Name/Lvl/Class/Outcome at fixed pixel offsets) needed
content.width >= 625to fit with the 15-px row scrollbar gutter, but the window defaulted to 620, leaving the outcome column overlapping where the scrollbar appears once entries exceed visible rows. BumpedHISTORY_Wto 660 with explicit width math documenting the dependency between column widths and minimum frame width. - Statistics window resizable but content was fixed-size — could be shrunk past where the LibGraph instance and totals labels fit, leaving the graph clipped or empty space at larger sizes. Made non-resizable at 600×575; sizer grips hidden and mouse scripts cleared so dragging the corner can't trigger anything.
- Settings window resizable but content used absolute SetPoint — shrinking horizontally clipped widgets to the right edge ("text floats outside"). Made non-resizable at 900×637; sizer grips hidden. The Game Version label inside the Main page also stopped wrapping (
SetWordWrap(false) + SetMaxLines(1)plus an explicit 600-px width becauseSetFullWidth(true)is a no-op when the parent group usesSetLayout("NIL")). - History tray class column wrapped to a second line for long class names —
lvlClassFontString had width 56 and word-wrap enabled, so "60 Demon Hunter" / "60 DeathKnight" wrapped. Bumped width to 110 and disabled word/non-space wrap withSetMaxLines(1). The name column auto-shrinks to keep the row geometry consistent. - Main window title showed the raw build tag (
Fast Guild Invite v.FastGuildInvite-v1.9.1) — the BigWigs packager replaces## Version: FastGuildInvite-v2.1.12in the TOC with the full git tag (FastGuildInvite-v1.9.1), andmainFrame:SetTitle("Fast Guild Invite v."..addon.version)then concatenatedv.in front, producing the doubled prefix; fixed by stripping theFastGuildInvite-vprefix fromaddon.versiononce at load time in init.lua and changing the title strings in mainFrame.lua and intro.lua to"Fast Guild Invite v"..addon.version, so the displayed string is nowFast Guild Invite v1.9.2. - Reset confirmation dialog never appeared even with
confirmSearchClearenabled —interface.confirmClearFramewas created viaGUI:Create("ClearFrame")but never had aSetPointanywhere except in the rarely-used/fgi resetWindowsPosslash command; AceGUI'sClearFrame:OnAcquirecallsShow()once, but a frame without an anchor renders with undefined geometry, so subsequentShow()calls had no visible effect; fixed by adding position restoration in the PLAYER_LOGIN handler (mirroring the existingmainFramepattern) — restores fromDB.global.confirmClearFrameif saved, elseSetPoint("CENTER", UIParent)— so the dialog always has an anchor and Show()/Hide() now toggles a visible widget. - Reset button did not clear the Found / Sent / Accepted / Filtered counters or the row list —
clearSearch()resetaddon.search.{inviteList, state, progress, timeShift, tempSendedInvites, whoQueryList}, the scrollbar offset, and the progress bar, but skipped theaddon.searchInfosession counters; per CLAUDE.md these must be set through the metamethod setter so the main-frame display refreshes via itsupdate()callback; fixed by addingaddon.searchInfo.{unique,sended,invited,filtered,decline,autodecline,search}(0)calls inclearSearch()(then==0branch of the metamethod zeroes the counter viaself[1] + (-self[1])). - "Accepted" count never incremented on Classic Era — the join handler only bumped the counter when
DB.realm.alreadySended[normalizedName]was set, but Classic Era never firesERR_GUILD_INVITE_Sto the inviter so the "invite" handler path that callsrememberPlayer()never runs on accept (only on decline); on a successful accept the anti-spam list stayed empty for that name and the counter lookup missed; fixed by also checkingaddon.pendingInvites[normalizedName]in the join handler — the entry is set atGuildInvite()time and only cleared on decline / not-found / accept, so its presence at join time is a reliable signal; if found, increment Accepted, log the invite, persist toalreadySended(for non-type-3 invites), and clear the pending slot. - Scrollbar in player list was always visible even when the list fit the viewport —
mainFrame.listScrollBarwas created withSetMinMaxValues(0, 0)and never explicitly hidden; fixed infn.onListUpdate()(where the scrollbar range is already recomputed each refresh) by callingsb:SetShown(maxValue > 0)so the bar disappears whenever#list ≤ rowCount.
Internal
- New file compactFrame.lua — owns
interface.compactFrame. PlainCreateFrame(no AceGUI border), HIGH strata +SetToplevel(true)+SetFrameLevel(200), semi-transparent black background, draggable title row, refresh hooked into both theaddon.searchInfocounter metamethod andfn.onListUpdate()so counters and queue rows track scan activity in real time. Loaded aftermainFrame.luain all five TOCs. mainFrame.applyCompactMode()— single mutator that swaps visibility betweeninterface.mainFrameandinterface.compactFrame. No-op when neither is shown so PLAYER_LOGIN doesn't pop a window the user wasn't looking at. Replaces the previous in-place "shrink the AceGUI window" approach.fn.showAddon()— new helper used by/fgi showand the Settings close button so they bring up whichever frame the current mode wants instead of hard-codingmainFrame:Show().mainFrameToggle()in FGI_Core.lua — minimap LMB now checksDB.global.compactModeand toggles the compact tray when in that mode.DB.factionrealm.history.invites— new per-invite log array with rich metadata ({time, name, lvl, race, class, outcome}); trimmed toDB.global.historyRetentionDayson every write.addon.invitesInFlight— runtime-only map keyed by normalized player name, populated at send time with{time, name, lvl, race, class}; consumed byfn.history:logInvite(outcome, name)so accept/decline/antispam outcomes inherit the level/race/class captured at send. Cleared on resolution.fn.history.logInvite(outcome, name, fallback)— new sink for per-invite events; outcomes areaccepted,declined,antispam,blacklisted. Called from the join handler (functions.lua), the decline / auto-decline handlers (Scan.lua), and the new row-level Blacklist button (mainFrame.lua).fn.history.trim()— single-pass retention enforcement; runs on everylogInvite()and at PLAYER_LOGIN. Appends are time-ordered so the implementation linear-scans for the first kept index and slices once, with a fast path when everything is older than the cutoff.- New file inviteHistory.lua — owns
interface.historyFrame, the row template, andfn.history.refreshHistoryPage(); loaded afterstatistic.luain the TOC.FGIHistoryFrameglobal is registered withUISpecialFrames(viafn.updateEscFrames()) so ESC closes it like the other addon windows. /fgi resetWindowsPos— now also resetsinterface.compactFrameto CENTER and clearsDB.global.compactFrame, alongside the other window resets.- New dev-only wow-version-replication.ps1 — file watcher that mirrors the source tree from
_classic_era_/Interface/AddOns/fastguildinvite/into the_classic_,_anniversary_, and_retail_installs as you edit. Reads.pkgmeta'signore:list at startup so the synced output looks like the BigWigs-packaged release (no.git, no IDE metadata, no docs). Excluded from the packager via the existing**/*.ps1.pkgmetarule. - Locale keys added —
lastScan,nextScan,scanNone,compactMode,compactModeTooltip,compactSymbol,expandSymbol,compactToggleTooltip,historyRetentionDays,historyRetentionDaysTooltip,historyTitle,historyBtn,historyBtnTooltip,historyCol*,historyOutcome*,historyEmpty,historyFooterCount*,rowBlacklistTooltip— added to bothenUS.luaandruRU.lua.
[v1.9.1] (2026-04-19) - Main Window Size Persistence & Minimap Button Rebinding
Improvements
- Minimap button rebinding — LMB now opens/closes the main window (previously invited the front-of-queue player), RMB now opens/closes the settings window (previously opened the main window); Shift+LMB pause/continue is removed entirely; the main window already has a visible Pause/Play button and an Invite button, so the minimap-button shortcuts for those actions were redundant and easy to trigger by accident;
L["minimap"]tooltip updated in enUS, ruRU, deDE, frFR, zhCN, zhTW, and koKR to match the new two-line binding
Bug Fixes
- Main window size not persisted across reloads — window position saved correctly because the title bar's
OnMouseUpwas overridden to writeDB.global.mainFrame.{point,xOfs,yOfs,width,height}, but resizing uses three separate grip frames on theClearFramewidget (sizer_se,sizer_s,sizer_e— seeLibs/GUI.lua), each bound to the library's defaultMoverSizer_OnMouseUp, which only updates the widget's internalstatustable and never touchesDB.global.mainFrame; so releasing a resize drag left the saved width/height at their previous (or default) values and next login restored the old size; fixed by factoring the title-bar persistence logic into a localpersistMainFrameGeomhelper and binding it to all three sizer grips'OnMouseUpas well, so any resize release now writes the same DB entry the move release already did
[v1.9.0] (2026-04-19) - Main Window Consolidation & Player Queue List
New Features
- feat: Scrollable player queue list — the main window now shows all scan candidates at once in a scrollable list (Name / Lvl / Class columns) with per-row action icons; any row can be acted on, not just the front of the queue; the list scrolls with the mouse wheel; player names are coloured by class; right-click any name opens the same context menu (Invite / Blacklist / Unblacklist) that was previously on the scan popup; RaiderIO profile tooltip appears on hover when RaiderIO is installed
- feat: Progress bar in main window — the scan progress bar (queries completed / total) is now embedded in the main window between the filter controls and the player list; it was previously only visible in the separate scan popup
- feat: Compact stats row — a single-line summary row (
Found: N Sent: N Accepted: N Filtered: N) appears below the progress bar and updates in real time as the scan runs - feat: Scan controls integrated into main window — the Start/Pause button and countdown timer label are now part of the main window's bottom button row; the scan popup no longer appears
- feat: Per-row Skip button — a third per-row action (between Invite and Decline) that removes a player from the current queue without blacklisting; Decline still respects the existing
Remember skipped/Remember allsettings, so Skip is the non-destructive option when a player should just be passed over for this cycle - feat: 3D spinner widget for the level filter — the Level Filter min/max numbers are now rendered in an outlined, drop-shadowed font with ▲/▼ spinner buttons above and below each value; mouse wheel still works (Shift = step of 5) and clicking the arrows increments/decrements; makes it obvious the range is interactive instead of looking like a static label
Improvements
- Flat icon buttons for per-row actions — the per-row Invite / Skip / Decline buttons are now flat 18×18 icon buttons using WoW's built-in ReadyCheck textures (green check / yellow
?/ red X), replacing the beveledTButtonwidgets; they sit vertically centered against row text via a 2-px offset - Enriched multi-line tooltips on every main-window button — Close, Invite
+(N), Pause/Play, Decline, Settings, Statistics, and all three per-row icons now have descriptive two-line tooltips explaining what the button does and relevant side effects (e.g. that Decline consults theRemember skippedsetting, that Skip does not); new locale keyscloseBtnTooltip,inviteBtnTooltip,pausePlayBtnTooltip,declineBtnTooltip,settingsBtnTooltip,statisticBtnTooltip,rowInviteTooltip,rowSkipTooltip,rowDeclineTooltipin both enUS and ruRU; original short labels are kept untouched for use as button text elsewhere - Top-row labels on the same horizontal line — "Level Filter" and "Invitation Mode" now share the same Y, and the level spinners are vertically centered with the mode dropdown below them; the panel reads as a single top row rather than two staggered mini-panels
- Renamed "Level Range" → "Level Filter" — more clearly conveys that the range actually filters WHO query results (it has always been wired up, but "Range" read as a static display); also capitalised "Invitation mode" → "Invitation Mode" for consistency
- Main window resizes with content — a new
OnSizeChangedhook on the outer frame restretches the progress bar, stats label, and bottom button group so the UI actually uses the extra horizontal space when the window is dragged wider;SetProgressnow remembers the last value so the bar doesn't reset to empty during a resize - Symmetric top/bottom padding inside the scroll list — 4-px padding on top/bottom (
LIST_PAD_TOP/LIST_PAD_BOTTOM) keeps descenders likeg,p,jfrom clipping the container edge;relayoutList()shrinks the list container after each resize so the bottom buffer always matches the top exactly, rather than growing up to 19 px of dead space fromfloor()truncation - Minimum window dimensions raised —
SetResizeBounds(580, 450)so (a) the scrollbar can no longer overlap the row's rightmost decline icon, and (b) saved sizes from before the queue list redesign are clamped upward on load, preventing a collapsed list area on older profiles - Dark semi-transparent background on the list container — a 30 % black texture over
listBGso the list area is always visually obvious, even while empty
Bug Fixes
UIPanelScrollBarTemplatecrashed the load (SetVerticalScroll (a nil value)atSecureScrollTemplates.lua:24) — the template'sOnValueChangedscript callsSetVerticalScrollon an associatedScrollFrame, but our list container is a plainFrameso the call errored on the firstSetValue(0)and halted the rest ofmainFrame.lua(row loop, button group, PLAYER_LOGIN handler); fixed by replacing the templated slider with a bareCreateFrame("Slider")plus manual thumb and background textures — we don't need scroll-frame integration because the list uses virtual row windowing, not native scroll- List rows invisible even though the window loaded —
listBG's TOPLEFT anchored tocolName.frame.BOTTOMLEFT, butTLabel.UpdateImageAnchorderives the frame height fromFontString:GetStringHeight()which can return 0 at load time before font metrics are ready (clamped to 1 px); combined with any anchor chain fragility this collapsedlistBGto zero height; fixed by anchoringlistBG.TOPLEFTdirectly tosearchInfo.frame, "BOTTOMLEFT", -5, -30— a fixed offset independent of TLabel font metrics - Saved window height below the new minimum produced a clipped/collapsed list — profiles from before the queue-list redesign could store a
DB.global.mainFrame.heightmuch smaller than 450; the load-time SetPoints would then placelistBG's bottom above its top, giving negative height and hiding every row; fixed by clamping the loaded size withmath.max(580, width)/math.max(450, height)in the PLAYER_LOGIN handler so older profiles are migrated silently - Retail: error spam
attempt to index local 'msg' (a secret string value tainted by 'FastGuildInvite')(100+ times per session) —functions.lua:520registers aCHAT_MSG_SYSTEMhandler that tries to detect guild-join messages viamsg:match("^(.+) has joined the guild%.$"); on Retail 11.x,CHAT_MSG_SYSTEMpayloads are now flagged as "secret strings" and any:method()call (which indexes the string via its metatable) raises a taint error; the Classic code path usingERR_GUILD_JOIN_Swas already guarded withnot gv.isRetail, but the Retail fallback was unguarded, so every single system message (loot rolls, zone changes, achievement announcements, etc. — not just guild-joins) hit the unsafemsg:matchand errored; fixed by adding the samenot gv.isRetailguard to the fallback; guild-join detection on Retail now returns early — the feature was not actually functional there due to the taint error, so no working behaviour is lost, and re-adding it properly would require switching toGUILD_ROSTER_UPDATEdiffing
Removals
- Retired floating scan popup (FGIScanFrame) — the compact always-visible scan overlay is removed; all its functionality (progress bar, player display, invite/decline buttons, pause/start control) has been absorbed into the enriched main window; the "Customize Interface" settings tab that toggled scan-popup sub-elements is also removed
[v1.8.0] (2026-04-19) - Sync Broadcast Safety Overhaul
New Features
- feat: Whisper welcome to new guild members — added a second welcome control to the Guild settings tab: a toggle ("Send whisper to new guild members") and a text box that fires a private whisper to the new member 2.5 seconds after they join, using the same
NAMEsubstitution token as the existing guild chat welcome; entirely independent — either message can be enabled or disabled on its own; usesC_ChatInfo.SendChatMessageon Retail andSendChatMessageon Classic/TBC/Wrath/Cata - feat: Session and all-time stats panel in Statistics window — the Statistics graph window now shows two summary rows below the graph: all-time totals (searches, found, sent, accepted, rejected — persisted per faction-realm from the date tracking began) and current-session totals; session tracking is expanded to include declines and auto-declines in addition to the existing sent/accepted counters;
DB.factionrealm.totalsstores the persistent counters and reuses all existing locale strings - feat: ESC keep-open toggle — pressing Esc now closes the main window, scan frame, and settings window; a new "Keep open on Esc" checkbox in Settings > Main restores the original no-op behaviour when checked; implemented via
UISpecialFrames— each FGI window is assigned a global name (FGIMainFrame,FGIScanFrame,FGISettingsFrame) and added to or removed from the list byfn.updateEscFrames(); stored inDB.global.keepOpen - feat: Invite testing mode — a new "Invite testing mode" checkbox in Settings > Main simulates the full invite flow without sending any guild invite or whisper; when enabled, clicking Invite prints what would have been sent to chat (player name + invite mode), still increments session/all-time counters, and removes the player from the queue; stored in
DB.global.testingMode - feat: Configurable scan interval — the WHO query interval (previously hardcoded at 5 s via
FGI_SCANINTERVALTIME) is now user-adjustable (2–30 s) via a new "Scan interval (sec)" edit box in Settings > Main; the value is applied immediately on change viafn.setScanInterval()and persisted inDB.global.scanInterval; the constant is kept as the load-time fallback until DB is ready at PLAYER_LOGIN - feat: Filter overwrite confirmation — saving a filter with a name that already exists now shows a yes/no StaticPopup ("Filter already exists. Overwrite it?") instead of blocking with a "name is busy" error; confirming re-runs the full validation pass and writes the new data over the existing entry
Bug Fixes
- ESC had no effect on any FGI window — initial implementation used
EnableKeyboard(true)+OnKeyDown, which only fires on frames that hold keyboard focus; plainFramewidgets never receive keyboard events this way; fixed by usingUISpecialFrames— WoW's own ESC handler iterates this list and hides the topmost visible frame; the settings window was also missing from the original implementation and now closes on ESC as well - Direct
checkSynccalls bypassed all sync safety guards (SYNC-021) — two code paths calledcheckSync(CHANNEL_MOD)directly instead of going throughfn.startSync(): one after every successful sync in CHECK state, and one after any blacklist entry was removed;checkSynconly broadcasts the hash — it does not rebuildSync.tablesForSync, check combat state, or guard against an already-in-progress session; iffn.startSync()was skipped at login (e.g. player was in combat),Sync.tablesForSyncwould be nil, causing a Lua crash insidegetTotalHash()the next time any FGI guild member triggered a broadcast; the post-success direct call also caused cascading guild-wide broadcasts on large guilds: every completed sync immediately re-broadcast to all peers, which each responded if their hash differed, creating a chain of back-to-back sync sessions with no idle window; the blacklist-removal call had noSync.tablesForSyncnil guard at all; fixed by replacing both directcheckSync(CHANNEL_MOD)calls withfn.startSync(true), which rebuildsSync.tablesForSyncfrom the current DB, respects theSync.target == ''in-progress guard, and skips the broadcast if the player is in combat
[v1.7.11] (2026-04-18) - Multi-Table Sync Stall & Stale Message Guard
Bug Fixes
- TBC:
Sync failed: disconnectedspam when 2+ tables are out of sync (SYNC-019) — after the first mismatched table was transferred and both sides reached CHECK state, the receiver sent{msg=0}with the updated total hash; an early-dispatch block interceptedmsg=0before the state machine and, on mismatch, printed a debug line and returned without doing anything; the CHECK state handler that drives the next sync round was therefore dead code in this path; both sides sat in CHECK until the 10-second timeout firedcloseConnect(), printed "Sync failed", and re-queued the session — an infinite loop for any guild with 2+ mismatched tables; fixed by adding explicit continue-sync logic to themsg=0mismatch branch: when in CHECK state, re-enter LISTEN and sendinitSync+choose_tableto advance to the next table - Stale
msg=2in ESTABLISHED or CHECK state could corrupt the transfer (SYNC-020) — the same early-dispatch block processedmsg=2(settings/header) from any state and any sender with no guard; a delayed or replayed settings message arriving while already ESTABLISHED would silently overwriteSync.type, stop the active timeout, and fire a secondapprove_settings, causingSendSyncAddonStream()to run twice and corrupt the receiver's chunk reassembly; fixed by adding a LISTEN-only state guard and a sender-identity check to themsg=2early-dispatch handler
Improvements
- Settings > Main: added tooltips to all checkboxes and dropdowns —
minimapButtonnow describes left-click / shift-left-click / right-click behaviour;createMenuButtonsclarifies the chat context menu feature and reload requirement;queueNotify,searchAlertNotify, andconfirmSearchCleareach received new descriptive tooltips;showUpdateInfo(Updates) explains the changelog popup behaviour and developer data requirement;clearDBtimes(Player memorization time) explains the anti-spam list expiry options via a custom hover hook on the AceGUI Dropdown frame
[v1.7.10] (2026-04-18) - Bidirectional Sync Race Fix
Bug Fixes
- TBC:
Sync failed: disconnectedspam — bidirectional sync race condition (SYNC-018) — when two FGI clients both detect a hash mismatch on each other's guild broadcast simultaneously, both run the size comparison at the same time; the side with more data correctly transitions LISTEN→ESTABLISHED and sendsmsg=2settings, while the other side — also correctly — sendsapprove_connectto ask the sender to proceed; these two messages cross in transit; when the sender (already in ESTABLISHED) received theapprove_connect, the unguarded handler fired and sent a secondmsg=2, causing the receiver to emit twoapprove_settingsresponses;SendSyncAddonStream()was then called twice, sending duplicate or shifted data chunks; the receiver's reassembly failed to decode, it calledcloseConnect()and sentCLOSE_CONNECTback, which printed "Sync failed: <player> disconnected" on the sender's side; sincecloseConnect()re-queues and re-broadcasts 1 second later, the cycle repeated indefinitely; debug mode appearing to "fix" the issue was coincidental — the partner actually disconnected shortly afterwards; fixed by adding LISTEN-only guard toapprove_connecthandler and ESTABLISHED-only guard toapprove_settingshandler so crossed-in-transit messages are silently dropped
[v1.7.9] (2026-04-17) - Sync Timeout Leak Fix
Bug Fixes
- TBC:
Sync failed: disconnectedfalse positives during normal sync (SYNC-017) —Sync.timeout.new()created a newC_Timerbut never cancelled the previous one; every call in the LISTEN state—one per table hash round-trip—leaked an orphaned timer; with 4 sync tables and matching hashes, up to 4 orphaned timers accumulated per sync session; the oldest orphaned timer firedcloseConnect()10 seconds after it was created, even if the sync had already advanced to ESTABLISHED or CHECK state;closeConnect()sawwasEstablished=trueandwasSuccess=niland printed the false failure message; the symptom was worse on TBC because larger, more active guilds generate more data across all tables, producing more LISTEN-state round trips and more leaked timers; fixed by adding an explicittimer:Cancel()guard at the top ofSync.timeout.new()before allocating a new timer
[v1.7.8] (2026-04-15) - Delayed Guild Welcome Message
New Features
- Delayed auto-welcome message on guild join — the welcome message sent to guild chat when a new member joins is now deferred by 2.5 seconds via
C_Timer.After(); previously the message fired immediately on theCHAT_MSG_SYSTEMevent, before the new member's client had fully settled into the guild channel;SendChatMessageis not hardware-event-protected so the timer callback does not raise a Lua protected-call error; the player name token (NAME) is resolved before the timer starts so there is no stale-closure risk
[v1.7.7] (2026-04-14) - Combat Sync Fix, Anti-Spam Accuracy, Scan State Cleanup
Bug Fixes
- TBC:
Sync failed: disconnectedspam and screen stutter during combat (SYNC-016) —getTableHash()andgetTotalHash()each calledIsInCombat(true), which invokedSync.closeConnect()as a side effect of computing a hash; the resulting CLOSED-state reset left a wandering 10-second timer that re-killed any new sync started 1 second later, looping indefinitely; every closeConnect cascade (ChatThrottleLib flush + timer cancel + chat print + UI refresh) ran synchronously inCHAT_MSG_ADDON OnEvent, dropping a frame on each cycle; removed theIsInCombatlocal function entirely; combat is now checked only atfn.startSync()entry viaUnitAffectingCombat("player"), which prevents initiating a new sync in combat without ever aborting an in-flight one - Stale scan results from previous sessions caused invites to offline players (SCAN-006) —
saveSearchwrote the entireaddon.searchtable (includinginviteList) to SavedVariables after every WHO callback and restored it on login; WHO results are an online-only snapshot with no value after the session ends, and WoW provides no reliable way to distinguish/reloadfrom logout, so players who logged off hours ago could remain in the queue and receive broken invite attempts; removedsaveSearchentirely —addon.searchis now always freshly initialised on login; the corresponding Settings checkbox, AceDB default, and analytics call are also removed - Offline players added to anti-spam list; players never added on Classic Era;
not_foundcleanup incorrect on cluster realms (SCAN-007) —fn:rememberPlayer()was called beforeGuildInvite()returned, writing failed invites to the anti-spam list; on Classic EraERR_GUILD_INVITE_Sis never sent to the inviter's client so the original"invite"-only handler never fired and players were never added to the list at all;not_foundcleanup usednormalizePlayerNameon the bare server name which is a no-op on Classic, missing a cluster-realm key like"Name-OtherRealm"; addedaddon.pendingInviteskeyed bynormalizePlayerName(playerName); types 1/2/4 deferrememberPlayerto server confirmation;"decline"and"auto_decline"handlers now also flushpendingInvites(covering Classic Era);not_foundusesnext(pendingInvites)since only one invite can be in-flight at a time
[v1.7.6] (2026-04-13) - Retail Anti-Spam & Invite Fixes
Bug Fixes
- Retail: anti-spam list silently cleared after every same-realm invite (SCAN-004) — on Retail,
C_GuildInfo.InviteandC_ChatInfo.SendChatMessagereject"Name-MyRealm"for same-realm players and fire anot_founderror event;scanFrame.pausePlayFilterwas handling that event by deletingDB.realm.alreadySended[normalizedName], erasing the entry thatrememberPlayer()had just written; only cross-realm players (~1 in 10 WHO results) were stored correctly because their realm suffix was already different; fixed by stripping the self-realm suffix inAPI.GuildInvite()andfn:sendWhisper()before calling the WoW API on Retail so the API call succeeds and nonot_foundevent is raised - Retail:
blackListAutoKickcrashes on every guild join (SCAN-005) —fn:blackListAutoKick()registered aCHAT_MSG_SYSTEMlistener that calledstrfind(ERR_GUILD_JOIN_S, ...)directly, the same protected C-side string that caused the v1.7.5 crash in the main handler; the auto-kick handler was missed in that fix; now guarded withnot gv.isRetailand falls through to the same English text-pattern fallback used by the main handler - Anti-Spam List panel not refreshing in real time (UI-003) —
fn:rememberPlayer()wrote toDB.realm.alreadySendedbut never notified the panel; entries were only visible after switching settings tabs;rememberPlayer()now callsantiSpamList:update()immediately when the panel is visible;settings.ShowContent()also callsupdate()when either the Anti-Spam List or Blacklist tab is opened so bulk sync arrivals are reflected without a second tab switch - Anti-spam timestamps always show
:00for minutes and seconds (UI-002) —fn.getTime()calledtime({year, month, day, hour})withminandsecomitted; Lua'stime()defaults omitted fields to0, so every stored timestamp wasHH:00:00;minandsecare now included
[v1.7.5] (2026-04-13) - Retail Secret String Crash Fix
Bug Fixes
- Lua crash on guild join in Retail —
ERR_GUILD_JOIN_Sis a protected C-side "secret string" in Retail WoW; callingstrfind()on it raisedattempt to perform string conversion on a secret string value(642 times per session per report); the ClassicERR_GUILD_JOIN_Spath is now skipped on Retail, which already has a working fallback pattern match for the "has joined the guild" message
[v1.7.4] (2026-04-13) - Add to Blacklist from Anti-Spam List
New Features
- Add to Blacklist from Anti-Spam List — right-clicking a player name in the Anti-Spam List window now shows an "Add to Blacklist" option alongside the existing Delete option; adds the player to the blacklist (with the configured default reason, chat confirmation, and sync propagation) while leaving them in the anti-spam list unchanged
Bug Fixes
- Blank "Add to Blacklist" button text — the locale key
"Добавить в черный список"was missing fromruRU.lua, which is the master key source for the locale merge insummary.lua; all keys absent fromruRUare silently omitted from the finalLtable, causing the button to display no text
[v1.7.3] (2026-04-13) - Scan Window Resize Fix
Bug Fixes
- Scan window resize clears player display (SCAN-003) — resizing the scan frame while a scan was running caused the player name label to go blank and the invite count to disappear, causing missed invitations; the
OnSizeChangedhook now callsonListUpdate()after each size event to re-sync the display withinviteList
[v1.7.2] (2026-04-11) - Sync Stability & Multi-Peer Queue
Bug Fixes
- Sync sender false failure message (SYNC-014) — the peer who sent data during a sync always received a red failure message in chat even when the sync completed successfully;
CLOSE_CONNECTreceived while in CHECK state is now correctly treated as a clean success - Second peer disrupts in-progress sync (SYNC-015) — a competing
initSyncfrom a second peer while already syncing caused the first sync to be torn down mid-handshake; the second peer is now refused gracefully and queued - Sync queue implementation — refused peers are added to
Sync.queue; when the current session ends, a re-broadcast is triggered automatically so every online peer eventually gets synced without manual intervention - Nil crash in
onSyncSuccess(SYNC-014 follow-up) — automatic login sync triggeredonSyncSuccesswithoutonSyncStartedever being called, leavingsyncPreCounts.blackListnil and causing a Lua arithmetic error; count diff is now skipped whensyncPreCountswas not initialised
[v1.7.1] (2026-04-11) - Settings Tab Highlight & Sync Feedback
New Features
- Active settings tab highlight — the left-hand navigation buttons in the Settings window now visually indicate which panel is currently open; the active button stays highlighted using WoW's native
LockHighlight/UnlockHighlightbutton methods - Sync status messages — pressing the Sync button now prints status messages to the default chat frame with the
<FGI>prefix; messages are suppressed when "Disable addon messages" is enabled- Yellow
Syncing...on broadcast - Green
Synced with <player> (+N entries)or(already up to date)on success - Red
Sync failed: <player> disconnectedon timeout or mid-sync disconnect - Grey
Already up to date.when no guild peer responded to the broadcast
- Yellow
Bug Fixes
- False sync failure message on sender side —
CLOSE_CONNECTreceived while in CHECK state is now treated as a successful close; previously the sending peer always printed a failure message even when the sync completed normally
[v1.7.0] (2026-04-10) - Blacklist Sync Overhaul & Class Filter Fix
New Features
- Blacklist removal propagation — removing a player from the blacklist now syncs the removal to all online guild peers; a
blackListRemovedtombstone table tracks deletions so removed entries are never re-synced back by peers - Auto-broadcast on blacklist removal — triggers an immediate guild hash broadcast when a player is removed so online peers sync without requiring relog or manual sync
Bug Fixes
- Bidirectional cursor desync (SYNC-008) — the
choose_tableresponder was incorrectly sendingchoose_tableback, causing both peers to advance their cursors independently and compare the wrong tables against each other - Re-add clears tombstone — re-adding a previously removed player now correctly clears their tombstone so the re-addition propagates to peers
- Double-delete required to remove (SYNC-010) —
fn:blacklistRemovenow deletes the exact stored key before case variants, so a single remove always works - Lua nil errors on blacklist operations (SYNC-011) —
Sync,checkSync, andCHANNEL_MODare now forward-declared at the top offunctions.lua; they were previously nil when referenced by functions defined before the sync section - Blacklist UI not refreshing after sync (SYNC-012) — the blacklist panel now updates immediately when entries are added or removed via sync
- Class filter checkboxes version-gated — Death Knight filter shown for Wrath/Cata/Retail; Monk, Demon Hunter, and Evoker filters shown for Retail only; previously these checkboxes were absent on non-Classic-Era clients causing incorrect filter results
- Window resize minimum bounds (UI-001) — scan frame minimum resize now tracks actual visible content so it cannot be squished below its contents; main frame minimum width set to prevent the level range controls from overflowing the frame border; scan frame children (player label, progress bar) now resize with the frame
[v1.6.3] (2026-04-06) - Blacklist Sync & Manual Sync Button
New Features
- Blacklist Sync Between FGI Users —
DB.realm.blackListis now included in the peer-to-peer sync system alongside the leave and invited lists; hashes automatically on login and syncs when a mismatch is detected - Manual Sync Button — "Start Sync" button added to FGI Settings → Main tab; triggers a guild-wide hash broadcast and full sync handshake on demand without relogging
Changes
- Refactored login sync init into
fn.startSync()so it can be called externally (login and button share the same code path)
[v1.6.2] (2026-03-29) - Performance Fixes & Blacklist Auto-Advance
New Features
- Auto-advance invite queue after blacklisting — right-clicking to blacklist a player from the scan frame now automatically advances to the next candidate (both fast-blacklist and popup-confirmation paths)
Bug Fixes
- GuildRoster storm —
GuildRoster()is now guarded behind the note-setting check; was being called on every guild join even when note-setting was disabled - Ticker accumulation —
C_Timer.NewTickerintimeCallbackStartnow cancels the prior ticker before creating a new one, preventing N simultaneous tickers after N scan cycles removeMsgListmemory leak — matched entries are now removed and empty per-player slots are nilled instead of accumulating indefinitely
[v1.6.1] (2026-03-29) - Settings & Auto-Blacklist Fixes
Bug Fixes
- Duplicate blacklistOfficer checkbox — removed orphaned copy from the Main settings tab; canonical copy remains in the Guild tab
- False "player is in guild" popup — when auto-blacklisting a leaver, the roster scan (
blacklistKick) is now skipped viaskipKick=true, preventing a false positive popup for a player who just left
[v1.6.0] (2026-03-29) - Anti-Spam List UI & Auto-Blacklist Leavers
New Features
- Anti-Spam List Management UI — new settings tab to view and manage the invited-players list (
DB.realm.alreadySended)- Paginated display (13 entries per page) with player names and invitation timestamps
- Right-click context menu to remove individual players
- "Clear All" button with confirmation dialog
- Integrated with "Player memorization time" auto-cleanup setting
- Fully localized (English & Russian)
- Access via FGI Settings → "Anti-Spam List"
- Auto-blacklist leavers (all versions) —
CHAT_MSG_SYSTEMhandler now triggers auto-blacklist for all game versions;GUILD_EVENT_LOG_UPDATEregistered for all versions
Bug Fixes
blacklistOfficernot persisting — Guild tab setting now correctly saves and restores across sessions- Statistics period dropdown not saving — period selection now persists across reloads
processGuildEventLogcrash on Retail — now usesC_GuildInfo.GetGuildEventLog()on Retail instead of the missing global
[v1.5.19-beta] (2026-03-28) - Blacklist Auto-Kick Fixes
Bug Fixes
- Hook stacking on right-click (BL-001) —
AddHookClick()was called on every page turn inblackList:update(), stackingOnMouseDownhandlers and causing multiple context menus per right-click - Auto-kick never calling
GuildUninvite(BL-002) —FGI_BLACKLISTpopupOnAcceptnow correctly callsGuildUninvite(name); button labels updated to Kick/Skip;OnCanceladded to dequeue and advance
[v1.5.18-beta] (2026-03-27) - Remember Skipped Players
New Features
- "Remember skipped players" setting — when enabled, clicking Skip/Decline adds the player to the anti-spam list so they won't appear in future scans; defaults to off
- Right-click player name in scan frame — opens the FGI context menu (Guild Invite / Blacklist / Unblacklist), same as chat name right-click
[v1.5.17] (2026-03-27) - TBC/Wrath/Cata GuildRoster Fix
Bug Fixes
- Crash on guild join in TBC/Wrath/Cata (KEY-017) —
GuildRoster()global only exists in Classic Era; all other versions now correctly useC_GuildInfo.GuildRoster()
[v1.5.16] (2026-03-21) - Retail Guild Note Support
New Features
- Retail note-setting —
GuildRosterSetPublicNote/GuildRosterSetOfficerNoteconfirmed working in Retail; guild note controls fully functional on all versions
Bug Fixes
message()global missing in Retail (KEY-010/011) — replaced withaddon.API.ShowMessage()which falls back toUIErrorsFrame:AddMessage()on Retailfn.debugformat string crash — format string had 3%splaceholders but only 2 arguments; fixed missing|rreset argFGI_APICompatdebug calls using colon syntax — causedself(the functions table) to be passed as the message argument, triggering the error path every time- Guild join note: stale roster data —
GuildRoster()is now requested immediately on join so the member appears in cache beforesetNoteruns 5 seconds later - Note condition always true — was checking
DB.global.setNote ~= ""(boolean vs string); now correctly checksDB.global.noteText ~= "" GetNumGuildMembers()multi-return (KEY-016) — wrapped in parentheses to prevent the online-count being used as a loop step
[v1.5.13] (2026-03-20) - Retail Note API & Sync Fix
Bug Fixes
- Guild notes crash on Retail (KEY-014) —
SetPublicNote/SetOfficerNotenow useC_GuildInfo.SetNotewith correctisPublicNoteboolean on Retail C_GuildInfo.CanEditPublicNotemissing in Retail (KEY-012) — nil-guarded; returnstruewhen absent, deferring permission check to the server
[v1.5.12] (2026-03-19) - Sync De-taint & Stability
Bug Fixes
- Tainted sender string crash in Retail (KEY-013) —
senderinCHAT_MSG_ADDONis a Blizzard-protected secret string in Retail; replaced:match()withAmbiguate(sender, "none")to safely de-taint it Sync.timeout.timernil crash — added nil guards inrestoreSyncDefaultValues()andSync.timeout.stop()before calling:Cancel()- BCC interface line added to base TOC; CurseForge TOC creation re-enabled for multi-version packaging
[v1.5.8] (2026-03-13) - Multi-Version TOC
New Features
- TBC, Wrath, Cata Classic TOC files — created
FastGuildInvite_BCC.toc,FastGuildInvite_Wrath.toc,FastGuildInvite_Cata.tocfor proper CurseForge multi-version packaging
[v1.5.7] (2026-03-11) - Filter UI & Tooltip Fixes
Bug Fixes
- Fixed multi-line tooltip rendering in GUI widgets (FilterButton, TCheckBox, TLabel, TKeybinding)
- Fixed
FilterButtontooltip not displaying due to missing widget property - Fixed nil
letterFiltercausing tooltip errors inFiltersUpdate - Fixed
fn:split()nil crash - Improved spacing in filter creation UI (RaidProgress labels, bottom hint text)
- Localized new tooltips in English and Russian
[129] (2024-09-22)
- chat menu fix