0853d3854c
- Fix MailMailButton nil errors in SendMailFrame_CanSend and sendmail_clear - Keep a safe reference to the original Send Mail button - Add fallback handling for MailSubjectEditBox - Improve compatibility with UI/mailbox modifications - Update version to 1.4.8 - Update README and audit documentation
797 lines
25 KiB
Lua
797 lines
25 KiB
Lua
-- TurtleMailFix.lua
|
|
-- Compatibility/safety layer for TurtleMail 1.4.5 on Turtle WoW 1.18.x / Octo.
|
|
-- Loaded after TurtleMail.xml so it can replace fragile handlers before ADDON_LOADED/PLAYER_LOGIN.
|
|
|
|
if not TurtleMail then return end
|
|
|
|
local m = TurtleMail
|
|
local getn = table.getn
|
|
local function pack( ... ) return arg end
|
|
|
|
m.compat_version = "1.4.8"
|
|
|
|
local DEFAULT_SENT_FILTERS = {
|
|
Money = 1,
|
|
COD = 1,
|
|
Other = 1,
|
|
}
|
|
|
|
local DEFAULT_RECEIVED_FILTERS = {
|
|
Money = 1,
|
|
COD = 1,
|
|
Other = 1,
|
|
Returned = 1,
|
|
AH = 1,
|
|
AHSold = 1,
|
|
AHOutbid = 1,
|
|
AHWon = 1,
|
|
AHCancelled = 1,
|
|
AHExpired = 1,
|
|
}
|
|
|
|
local INBOX_AUCTIONHOUSES = {
|
|
[ "Stormwind Auction House" ] = true,
|
|
[ "Alliance Auction House" ] = true,
|
|
[ "Darnassus Auction House" ] = true,
|
|
[ "Undercity Auction House" ] = true,
|
|
[ "Thunder Bluff Auction House" ] = true,
|
|
[ "Horde Auction House" ] = true,
|
|
[ "Blackwater Auction House" ] = true,
|
|
}
|
|
|
|
local function merge_defaults( target, defaults )
|
|
for key, value in pairs( defaults ) do
|
|
if target[ key ] == nil then
|
|
target[ key ] = value
|
|
end
|
|
end
|
|
end
|
|
|
|
local function ensure_saved_variables()
|
|
if type( m.api.TurtleMail_Log ) ~= "table" then
|
|
m.api.TurtleMail_Log = {}
|
|
end
|
|
|
|
local log = m.api.TurtleMail_Log
|
|
if type( log.Sent ) ~= "table" then log.Sent = {} end
|
|
if type( log.Received ) ~= "table" then log.Received = {} end
|
|
if type( log.Settings ) ~= "table" then log.Settings = {} end
|
|
|
|
local settings = log.Settings
|
|
if settings.Enabled == nil then settings.Enabled = false end
|
|
|
|
if type( settings.SentFilters ) ~= "table" then settings.SentFilters = {} end
|
|
if type( settings.ReceivedFilters ) ~= "table" then settings.ReceivedFilters = {} end
|
|
merge_defaults( settings.SentFilters, DEFAULT_SENT_FILTERS )
|
|
merge_defaults( settings.ReceivedFilters, DEFAULT_RECEIVED_FILTERS )
|
|
|
|
if type( m.api.TurtleMail_AutoCompleteNames ) ~= "table" then
|
|
m.api.TurtleMail_AutoCompleteNames = {}
|
|
end
|
|
end
|
|
|
|
local function autocomplete_key()
|
|
local realm = m.api.GetCVar and m.api.GetCVar( "realmName" ) or "UnknownRealm"
|
|
local faction = m.api.UnitFactionGroup and m.api.UnitFactionGroup( "player" ) or "UnknownFaction"
|
|
return tostring( realm or "UnknownRealm" ) .. "|" .. tostring( faction or "UnknownFaction" )
|
|
end
|
|
|
|
local function ensure_autocomplete_table()
|
|
ensure_saved_variables()
|
|
local key = autocomplete_key()
|
|
if type( m.api.TurtleMail_AutoCompleteNames[ key ] ) ~= "table" then
|
|
m.api.TurtleMail_AutoCompleteNames[ key ] = {}
|
|
end
|
|
return key, m.api.TurtleMail_AutoCompleteNames[ key ]
|
|
end
|
|
|
|
local function sanitize_autocomplete_table()
|
|
local _, names = ensure_autocomplete_table()
|
|
for name, last_seen in pairs( names ) do
|
|
if type( name ) ~= "string" or type( last_seen ) ~= "number" then
|
|
names[ name ] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Turtle WoW/Octo can return a texture from CreateTexture without exposing it
|
|
-- through the expected global name. The original addon assumes the global exists.
|
|
local function ensure_horizontal_bars()
|
|
local frame = m.api.SendMailFrame
|
|
if not frame or not frame.CreateTexture then return false end
|
|
|
|
if not m.api.MailHorizontalBarLeft then
|
|
m.api.MailHorizontalBarLeft = frame:CreateTexture( nil, "BACKGROUND" )
|
|
end
|
|
if not m.api.MailHorizontalBarRight then
|
|
m.api.MailHorizontalBarRight = frame:CreateTexture( nil, "BACKGROUND" )
|
|
end
|
|
|
|
return m.api.MailHorizontalBarLeft and m.api.MailHorizontalBarRight
|
|
end
|
|
|
|
-- Upstream keeps the real Send button and subject edit box in ad-hoc globals
|
|
-- (MailMailButton / MailSubjectEditBox), then replaces the Blizzard globals with
|
|
-- proxy tables. If another addon changes those globals, or sendmail_load() runs
|
|
-- with an already-proxied widget, the saved globals can become nil and later
|
|
-- calls crash. Preserve strong references before the upstream swap happens.
|
|
local preserved_send_mail_button = m.api.SendMailMailButton
|
|
local preserved_subject_edit_box = m.api.SendMailSubjectEditBox
|
|
|
|
local function is_named_widget( widget, expected_name )
|
|
if not widget or type( widget.GetName ) ~= "function" then return false end
|
|
return widget:GetName() == expected_name
|
|
end
|
|
|
|
local function ensure_send_mail_button()
|
|
local button = preserved_send_mail_button
|
|
|
|
if not is_named_widget( button, "SendMailMailButton" ) then
|
|
button = m.real_send_mail_button
|
|
end
|
|
if not is_named_widget( button, "SendMailMailButton" ) then
|
|
button = MailMailButton
|
|
end
|
|
if not is_named_widget( button, "SendMailMailButton" ) then
|
|
button = m.api.SendMailMailButton
|
|
end
|
|
|
|
if is_named_widget( button, "SendMailMailButton" ) then
|
|
preserved_send_mail_button = button
|
|
m.real_send_mail_button = button
|
|
MailMailButton = button
|
|
return button
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function ensure_subject_edit_box()
|
|
local edit_box = preserved_subject_edit_box
|
|
|
|
if not is_named_widget( edit_box, "SendMailSubjectEditBox" ) then
|
|
edit_box = m.real_subject_edit_box
|
|
end
|
|
if not is_named_widget( edit_box, "SendMailSubjectEditBox" ) then
|
|
edit_box = MailSubjectEditBox
|
|
end
|
|
if not is_named_widget( edit_box, "SendMailSubjectEditBox" ) then
|
|
edit_box = m.api.SendMailSubjectEditBox
|
|
end
|
|
|
|
if is_named_widget( edit_box, "SendMailSubjectEditBox" ) then
|
|
preserved_subject_edit_box = edit_box
|
|
m.real_subject_edit_box = edit_box
|
|
MailSubjectEditBox = edit_box
|
|
return edit_box
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function restore_real_mail_widgets_for_upstream()
|
|
local button = ensure_send_mail_button()
|
|
local edit_box = ensure_subject_edit_box()
|
|
|
|
-- Only restore the Blizzard globals immediately before upstream sendmail_load().
|
|
-- That function intentionally replaces them with proxy tables afterwards.
|
|
if button then m.api.SendMailMailButton = button end
|
|
if edit_box then m.api.SendMailSubjectEditBox = edit_box end
|
|
end
|
|
|
|
-- SavedVariables are not guaranteed to have a valid schema. Repair them before
|
|
-- the original handlers touch nested fields.
|
|
do
|
|
local original = m.ADDON_LOADED
|
|
function m.ADDON_LOADED()
|
|
if arg1 == "TurtleMail" then
|
|
ensure_saved_variables()
|
|
end
|
|
if original then original() end
|
|
if arg1 == "TurtleMail" and m.info then
|
|
m.info( "Compatibility fixes loaded (|cffeda55f" .. m.compat_version .. "|r)." )
|
|
end
|
|
end
|
|
end
|
|
|
|
do
|
|
local original = m.PLAYER_LOGIN
|
|
function m.PLAYER_LOGIN()
|
|
ensure_saved_variables()
|
|
sanitize_autocomplete_table()
|
|
if original then original() end
|
|
end
|
|
end
|
|
|
|
do
|
|
local original = m.slash_command
|
|
function m.slash_command( args )
|
|
ensure_saved_variables()
|
|
if original then return original( args ) end
|
|
end
|
|
end
|
|
|
|
-- Make autocomplete resilient to missing/corrupt SavedVariables.
|
|
do
|
|
local original = m.add_auto_complete_name
|
|
function m.add_auto_complete_name( name )
|
|
if type( name ) ~= "string" or name == "" then return end
|
|
ensure_autocomplete_table()
|
|
if original then return original( name ) end
|
|
end
|
|
end
|
|
|
|
if GetSuggestions then
|
|
local original = GetSuggestions
|
|
function GetSuggestions()
|
|
ensure_autocomplete_table()
|
|
return original()
|
|
end
|
|
end
|
|
|
|
-- Ensure the two horizontal bar textures exist before the original setup/update
|
|
-- routines try to address them by global name.
|
|
do
|
|
local original = m.sendmail_load
|
|
function m.sendmail_load()
|
|
ensure_horizontal_bars()
|
|
restore_real_mail_widgets_for_upstream()
|
|
|
|
if original then
|
|
local result = original()
|
|
-- Upstream has now installed its proxy globals. Re-assert the private
|
|
-- references used by the legacy MailMailButton/MailSubjectEditBox code.
|
|
ensure_send_mail_button()
|
|
ensure_subject_edit_box()
|
|
return result
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Replace the fragile upstream CanSend hook. The original directly calls the
|
|
-- global MailMailButton and crashes when that saved global is nil.
|
|
if m.hooks then
|
|
m.hook.SendMailFrame_CanSend = function()
|
|
local button = ensure_send_mail_button()
|
|
if not button then return end
|
|
|
|
local name_box = m.api.SendMailNameEditBox
|
|
local send_money_button = m.api.SendMailSendMoneyButton
|
|
local send_money_frame = m.api.SendMailMoney
|
|
|
|
local recipient = name_box and name_box.GetText and name_box:GetText() or ""
|
|
local attached = m.sendmail_num_attachments and m.sendmail_num_attachments() or 0
|
|
local price = m.api.GetSendMailPrice and (tonumber( m.api.GetSendMailPrice() ) or 0) or 0
|
|
local player_money = m.api.GetMoney and (tonumber( m.api.GetMoney() ) or 0) or 0
|
|
local money = 0
|
|
|
|
if send_money_button and send_money_button.GetChecked and send_money_button:GetChecked()
|
|
and m.api.MoneyInputFrame_GetCopper and send_money_frame then
|
|
money = tonumber( m.api.MoneyInputFrame_GetCopper( send_money_frame ) ) or 0
|
|
end
|
|
|
|
local can_send = not m.sendmail_sending
|
|
and string.len( recipient ) > 0
|
|
and money + price * math.max( 1, attached ) <= player_money
|
|
|
|
if can_send then
|
|
button:Enable()
|
|
else
|
|
button:Disable()
|
|
end
|
|
end
|
|
end
|
|
|
|
if m.hooks and m.hooks.SendMailFrame_Update then
|
|
local original = m.hooks.SendMailFrame_Update
|
|
m.hook.SendMailFrame_Update = function( ... )
|
|
ensure_horizontal_bars()
|
|
return original( unpack( arg ) )
|
|
end
|
|
end
|
|
|
|
-- Ensure the legacy click handler sees a valid subject edit box and real send
|
|
-- button before it reads them. This protects the same upstream proxy hack from
|
|
-- producing a follow-up MailSubjectEditBox nil error.
|
|
do
|
|
local original = m.send_mail_button_onclick
|
|
function m.send_mail_button_onclick()
|
|
ensure_send_mail_button()
|
|
ensure_subject_edit_box()
|
|
if original then return original() end
|
|
end
|
|
end
|
|
|
|
-- Replace the upstream clear routine because it directly dereferences
|
|
-- MailMailButton and MailSubjectEditBox. Keep its behavior, but guard every UI
|
|
-- object so mailbox replacements cannot turn cleanup into another Lua error.
|
|
function m.sendmail_clear()
|
|
local any_item
|
|
|
|
for i = 1, 21 do
|
|
local attachment = m.api[ "MailAttachment" .. i ]
|
|
if attachment then
|
|
any_item = any_item or attachment.item
|
|
attachment.item = nil
|
|
end
|
|
end
|
|
|
|
if any_item and m.api.ClearCursor and m.api.PickupContainerItem then
|
|
m.api.ClearCursor()
|
|
m.api.PickupContainerItem( unpack( any_item ) )
|
|
m.api.ClearCursor()
|
|
end
|
|
|
|
local button = ensure_send_mail_button()
|
|
if button and button.Disable then button:Disable() end
|
|
|
|
local name_box = m.api.SendMailNameEditBox
|
|
if name_box then
|
|
if name_box.SetText then name_box:SetText( "" ) end
|
|
if name_box.SetFocus then name_box:SetFocus() end
|
|
end
|
|
|
|
local subject = ensure_subject_edit_box()
|
|
if subject and subject.SetText then subject:SetText( "" ) end
|
|
|
|
if m.api.SendMailBodyEditBox and m.api.SendMailBodyEditBox.SetText then
|
|
m.api.SendMailBodyEditBox:SetText( "" )
|
|
end
|
|
|
|
if m.api.MoneyInputFrame_ResetMoney and m.api.SendMailMoney then
|
|
m.api.MoneyInputFrame_ResetMoney( m.api.SendMailMoney )
|
|
end
|
|
|
|
if m.api.SendMailRadioButton_OnClick then
|
|
m.api.SendMailRadioButton_OnClick( 1 )
|
|
end
|
|
|
|
if m.api.SendMailFrame_Update then
|
|
m.api.SendMailFrame_Update()
|
|
end
|
|
end
|
|
|
|
-- Do not assume MailFrame exists during bag events.
|
|
function m.BAG_UPDATE()
|
|
if m.api.MailFrame and m.api.MailFrame:IsVisible() and m.api.SendMailFrame_Update then
|
|
m.api.SendMailFrame_Update()
|
|
end
|
|
end
|
|
|
|
-- Safer MAIL_SHOW: package-button regions differ between UI replacements.
|
|
function m.MAIL_SHOW()
|
|
-- Re-assert the legacy references whenever the mailbox opens. This also
|
|
-- recovers if another UI addon changed those globals after addon loading.
|
|
ensure_send_mail_button()
|
|
ensure_subject_edit_box()
|
|
|
|
if not m.api.MailFrame then return end
|
|
|
|
if m.api.TurtleMail_Point then
|
|
m.debug( "Set point" )
|
|
m.api.MailFrame:SetPoint( m.api.TurtleMail_Point.point, m.api.TurtleMail_Point.x, m.api.TurtleMail_Point.y )
|
|
end
|
|
|
|
if not m.first_show then
|
|
m.first_show = true
|
|
local package = m.api.SendMailPackageButton
|
|
|
|
if package then
|
|
if m.pfui_skin_enabled and m.api.pfUI and m.api.pfUI.api and m.api.pfUI.api.StripTextures then
|
|
m.api.pfUI.api.StripTextures( package )
|
|
end
|
|
|
|
local regions = { package:GetRegions() }
|
|
if regions[ 1 ] and regions[ 1 ].Hide then regions[ 1 ]:Hide() end
|
|
if regions[ 3 ] and regions[ 3 ].Hide then regions[ 3 ]:Hide() end
|
|
|
|
package:Disable()
|
|
package:SetScript( "OnReceiveDrag", nil )
|
|
package:SetScript( "OnDragStart", nil )
|
|
end
|
|
end
|
|
|
|
if m.api.MailFrameTab3 then
|
|
if m.log_enabled then
|
|
m.api.MailFrameTab3:Show()
|
|
else
|
|
m.api.MailFrameTab3:Hide()
|
|
end
|
|
end
|
|
|
|
m.timer = 0
|
|
m.money_received = 0
|
|
m.update_money( 0 )
|
|
end
|
|
|
|
-- Check bounds before querying inbox headers and tolerate nil COD values.
|
|
function m.on_update()
|
|
if not m.api.MailFrame or not m.api.MailFrame:IsVisible() then return end
|
|
|
|
if m._cursorItem then
|
|
m.debug( "on_update: cursorItem" )
|
|
m.cursorItem = m._cursorItem
|
|
m._cursorItem = nil
|
|
end
|
|
|
|
if m.sendmail_update then
|
|
m.debug( "on_update: sendmail" )
|
|
m.sendmail_update = nil
|
|
if m.sendmail_sending then
|
|
m.debug( "m.sendmail_sending" )
|
|
m.sendmail_send()
|
|
end
|
|
end
|
|
|
|
if m.inbox_update then
|
|
m.debug( "on_update: inbox_update" )
|
|
m.inbox_update = false
|
|
|
|
local index = tonumber( m.inbox_index ) or 1
|
|
local total = m.api.GetInboxNumItems and (m.api.GetInboxNumItems() or 0) or 0
|
|
|
|
if index > total then
|
|
if (tonumber( m.money_received ) or 0) > 0 then
|
|
m.info( string.format( "%s%s.", m.format_money( m.money_received ), L[ "collected" ] ) )
|
|
end
|
|
m.inbox_abort()
|
|
else
|
|
local _, _, _, _, _, cod, _, _, _, _, _, _, isGM = m.api.GetInboxHeaderInfo( index )
|
|
cod = tonumber( cod ) or 0
|
|
|
|
if m.inbox_skip or cod > 0 or isGM then
|
|
m.inbox_skip = false
|
|
m.inbox_index = index + 1
|
|
m.inbox_update = true
|
|
else
|
|
m.inbox_open( index )
|
|
end
|
|
end
|
|
end
|
|
|
|
if type( m.timer ) ~= "number" then m.timer = 0 end
|
|
if m.timer > 0 then
|
|
m.timer = m.timer - 1
|
|
elseif not m.inbox_opening and m.api.CheckInbox then
|
|
m.timer = 200
|
|
m.api.CheckInbox()
|
|
end
|
|
end
|
|
|
|
-- init() bound the original on_update function before this compatibility file was
|
|
-- loaded. Rebind the frame script so the safer implementation above is actually used.
|
|
if m.update_frame and m.update_frame.SetScript then
|
|
m.update_frame:SetScript( "OnUpdate", m.on_update )
|
|
end
|
|
|
|
-- Hide stale AH/returned icons on rows that no longer contain mail.
|
|
function m.MAIL_INBOX_UPDATE()
|
|
if m.inbox_opening then
|
|
m.inbox_update = true
|
|
end
|
|
|
|
if not m.api.InboxFrame then return end
|
|
local page = tonumber( m.api.InboxFrame.pageNum ) or 1
|
|
local total = m.api.GetInboxNumItems and (m.api.GetInboxNumItems() or 0) or 0
|
|
|
|
for i = 1, 7 do
|
|
local index = i + (page - 1) * 7
|
|
local auction_icon = m.api[ "TurtleMailAuctionIcon" .. i ]
|
|
local returned_icon = m.api[ "TurtleMailReturnedArrow" .. i ]
|
|
|
|
if index <= total then
|
|
local _, _, sender, _, _, _, _, _, _, was_returned = m.api.GetInboxHeaderInfo( index )
|
|
if auction_icon then
|
|
if INBOX_AUCTIONHOUSES[ sender ] then auction_icon:Show() else auction_icon:Hide() end
|
|
end
|
|
if returned_icon then
|
|
if was_returned then returned_icon:Show() else returned_icon:Hide() end
|
|
end
|
|
else
|
|
if auction_icon then auction_icon:Hide() end
|
|
if returned_icon then returned_icon:Hide() end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Guard frame lookups used while the mailbox is opening/closing.
|
|
function m.inbox_update_lock()
|
|
for i = 1, 7 do
|
|
local icon = m.api[ "MailItem" .. i .. "ButtonIcon" ]
|
|
local button = m.api[ "MailItem" .. i .. "Button" ]
|
|
|
|
if icon and icon.SetDesaturated then
|
|
icon:SetDesaturated( m.inbox_opening )
|
|
end
|
|
if m.inbox_opening and button and button.SetChecked then
|
|
button:SetChecked( nil )
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Locale-safe COD label.
|
|
function m.set_cod_text()
|
|
local cod_amount = m.api.COD_AMOUNT or "COD:"
|
|
local text = string.sub( cod_amount, 1, math.max( 0, string.len( cod_amount ) - 1 ) )
|
|
|
|
if not m.pfui_skin_enabled then
|
|
text = string.match( text, "^(.-)%s+%S+$" ) or text
|
|
end
|
|
|
|
if not m.api.SendMailMoneyText then return end
|
|
if m.api.SendMailCODAllButton and m.api.SendMailCODAllButton:GetChecked() then
|
|
m.api.SendMailMoneyText:SetText( text .. " " .. L[ "each mail" ] .. ":" )
|
|
else
|
|
m.api.SendMailMoneyText:SetText( text .. " " .. L[ "1st mail" ] .. ":" )
|
|
end
|
|
end
|
|
|
|
function m.update_money( money )
|
|
m.money_received = (tonumber( m.money_received ) or 0) + (tonumber( money ) or 0)
|
|
if not m.api.MoneyReceived then return end
|
|
|
|
m.api.MoneyReceived:SetText( L[ "Money received" ] .. ": " .. m.format_money( m.money_received ) )
|
|
if m.money_received > 0 then
|
|
m.api.MoneyReceived:Show()
|
|
else
|
|
m.api.MoneyReceived:Hide()
|
|
end
|
|
end
|
|
|
|
-- The original hook reads arg[3]/arg[12] from the function INPUT arguments.
|
|
-- GetInboxHeaderInfo receives an inbox index; sender/canReply are RETURN values.
|
|
-- Capture the return list first so sender autocomplete actually works.
|
|
if m.hooks and m.hooks.GetInboxHeaderInfo then
|
|
m.hook.GetInboxHeaderInfo = function( ... )
|
|
if not m.orig.GetInboxHeaderInfo then return end
|
|
local ret = pack( m.orig.GetInboxHeaderInfo( unpack( arg ) ) )
|
|
local sender = ret[ 3 ]
|
|
local canReply = ret[ 12 ]
|
|
if sender and canReply then
|
|
m.add_auto_complete_name( sender )
|
|
end
|
|
return unpack( ret )
|
|
end
|
|
end
|
|
|
|
-- Guard MailFrame lookup in attachment bookkeeping.
|
|
do
|
|
local original = m.sendmail_attached
|
|
function m.sendmail_attached( bag, slot )
|
|
if not m.api.MailFrame or not m.api.MailFrame:IsVisible() then return false end
|
|
if original then return original( bag, slot ) end
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Fix sent-money logging and tolerate malformed/system mail subjects.
|
|
function m.log.add( log_type, state )
|
|
if not m.log_enabled or type( state ) ~= "table" then return end
|
|
ensure_saved_variables()
|
|
m.debug( "Logging " .. tostring( log_type ) .. " message" )
|
|
|
|
local data = {
|
|
timestamp = time(),
|
|
icon = state.icon,
|
|
item = state.item,
|
|
}
|
|
|
|
local cod = tonumber( state.cod )
|
|
if cod and cod > 0 then data.cod = cod end
|
|
|
|
if log_type == "Sent" then
|
|
data.participant = state.to or ""
|
|
data.subject = state.sent_subject or state.subject or ""
|
|
|
|
local sent_money = tonumber( state.sent_money ) or 0
|
|
if not data.cod and sent_money > 0 then
|
|
data.money = sent_money
|
|
end
|
|
else
|
|
data.participant = state.from or ""
|
|
data.subject = state.subject or ""
|
|
data.returned = state.returned
|
|
data.gm = state.gm
|
|
|
|
local received_money = tonumber( state.money ) or 0
|
|
if received_money > 0 then data.money = received_money end
|
|
|
|
local function subject_matches( pattern )
|
|
if type( pattern ) ~= "string" or pattern == "" or data.subject == "" then return false end
|
|
local needle = string.gsub( pattern, "%%s", "" )
|
|
return needle ~= "" and string.find( data.subject, needle, 1, true ) ~= nil
|
|
end
|
|
|
|
if subject_matches( m.api.AUCTION_SOLD_MAIL_SUBJECT ) then
|
|
data.ah = "Sold"
|
|
elseif subject_matches( m.api.AUCTION_REMOVED_MAIL_SUBJECT ) then
|
|
data.ah = "Removed"
|
|
elseif subject_matches( m.api.AUCTION_EXPIRED_MAIL_SUBJECT ) then
|
|
data.ah = "Expired"
|
|
elseif subject_matches( m.api.AUCTION_WON_MAIL_SUBJECT ) then
|
|
data.ah = "Won"
|
|
elseif subject_matches( m.api.AUCTION_OUTBID_MAIL_SUBJECT ) then
|
|
data.ah = "Outbid"
|
|
end
|
|
end
|
|
|
|
if type( m.api.TurtleMail_Log[ log_type ] ) ~= "table" then
|
|
m.api.TurtleMail_Log[ log_type ] = {}
|
|
end
|
|
table.insert( m.api.TurtleMail_Log[ log_type ], data )
|
|
end
|
|
|
|
-- Calendar day buttons can retain an old .mails value or have no value at all.
|
|
-- Replace only the tooltip handler; calendar selection/layout remains upstream.
|
|
if m.calendar and m.calendar.show then
|
|
local original_show = m.calendar.show
|
|
|
|
local function calendar_on_enter( button )
|
|
return function()
|
|
local enabled = true
|
|
if button.IsEnabled then enabled = button:IsEnabled() end
|
|
local mails = tonumber( button.mails ) or 0
|
|
|
|
if enabled and mails > 0 then
|
|
m.api.GameTooltip:SetOwner( button, "ANCHOR_RIGHT" )
|
|
m.api.GameTooltip:SetText( mails .. " mail" .. (mails > 1 and "s" or ""), 1, 1, 1, 1, true )
|
|
m.api.GameTooltip:Show()
|
|
else
|
|
m.api.GameTooltip:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
m.calendar.show = function( data, current_date, anchor, on_select )
|
|
if type( data ) ~= "table" then data = {} end
|
|
original_show( data, current_date or time(), anchor, on_select )
|
|
|
|
for i = 1, 42 do
|
|
local button = m.api[ "TurtleMailCalendarDay" .. i .. "Button" ]
|
|
if button then
|
|
button:SetScript( "OnEnter", calendar_on_enter( button ) )
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Validated targeted fixes: persistent autocomplete timestamps and clean send abort.
|
|
-- Kept in a local block so integrating the former test file does not alter scope.
|
|
do
|
|
local m = TurtleMail
|
|
local getn = table.getn
|
|
local AUTOCOMPLETE_MAX_AGE = 60 * 60 * 24 * 30
|
|
local EPOCH_THRESHOLD = 1000000000
|
|
|
|
local function autocomplete_key()
|
|
local realm = m.api.GetCVar and m.api.GetCVar( "realmName" ) or "UnknownRealm"
|
|
local faction = m.api.UnitFactionGroup and m.api.UnitFactionGroup( "player" ) or "UnknownFaction"
|
|
return tostring( realm or "UnknownRealm" ) .. "|" .. tostring( faction or "UnknownFaction" )
|
|
end
|
|
|
|
local function ensure_autocomplete_table()
|
|
if type( m.api.TurtleMail_AutoCompleteNames ) ~= "table" then
|
|
m.api.TurtleMail_AutoCompleteNames = {}
|
|
end
|
|
|
|
local key = autocomplete_key()
|
|
if type( m.api.TurtleMail_AutoCompleteNames[ key ] ) ~= "table" then
|
|
m.api.TurtleMail_AutoCompleteNames[ key ] = {}
|
|
end
|
|
|
|
return m.api.TurtleMail_AutoCompleteNames[ key ]
|
|
end
|
|
|
|
-- Older TurtleMail builds stored GetTime() values here. GetTime() is session uptime,
|
|
-- so those values cannot be compared reliably across game/client restarts. Convert
|
|
-- legacy uptime-style values to a safe current timestamp, then expire names using time().
|
|
local function migrate_and_prune_autocomplete()
|
|
local names = ensure_autocomplete_table()
|
|
local now = time()
|
|
|
|
for name, last_seen in pairs( names ) do
|
|
if type( name ) ~= "string" or type( last_seen ) ~= "number" then
|
|
names[ name ] = nil
|
|
else
|
|
if last_seen < EPOCH_THRESHOLD then
|
|
last_seen = now
|
|
names[ name ] = last_seen
|
|
end
|
|
|
|
if now - last_seen > AUTOCOMPLETE_MAX_AGE then
|
|
names[ name ] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Store persistent timestamps for newly learned recipients.
|
|
function m.add_auto_complete_name( name )
|
|
if type( name ) ~= "string" or name == "" then return end
|
|
local names = ensure_autocomplete_table()
|
|
names[ name ] = time()
|
|
end
|
|
|
|
-- Keep the complete existing login flow, but migrate before the legacy cleanup
|
|
-- runs and prune again afterwards using persistent timestamps.
|
|
local original_player_login = m.PLAYER_LOGIN
|
|
function m.PLAYER_LOGIN()
|
|
migrate_and_prune_autocomplete()
|
|
if original_player_login then original_player_login() end
|
|
migrate_and_prune_autocomplete()
|
|
end
|
|
|
|
-- The send flow can remain stuck in sendmail_sending when an attachment
|
|
-- disappears or cannot be attached between queueing and the actual SendMail call.
|
|
-- Keep the upstream behavior, but explicitly abort the send state on that failure.
|
|
function m.sendmail_send()
|
|
if type( m.sendmail_state ) ~= "table" or type( m.sendmail_state.attachments ) ~= "table" then
|
|
m.sendmail_sending = false
|
|
m.sendmail_update = nil
|
|
return
|
|
end
|
|
|
|
local item = table.remove( m.sendmail_state.attachments, 1 )
|
|
if item then
|
|
m.api.ClearCursor()
|
|
m.orig.ClickSendMailItemButton()
|
|
m.api.ClearCursor()
|
|
m.orig.PickupContainerItem( unpack( item ) )
|
|
m.orig.ClickSendMailItemButton()
|
|
|
|
if not m.api.GetSendMailItem() then
|
|
m.api.DEFAULT_CHAT_FRAME:AddMessage( "|cffabd473TurtleMail|r: " .. m.api.ERROR_CAPS, 1, 0, 0 )
|
|
m.sendmail_sending = false
|
|
m.sendmail_update = nil
|
|
m.sendmail_state = nil
|
|
m.api.ClearCursor()
|
|
if m.api.SendMailFrame_Update then
|
|
m.api.SendMailFrame_Update()
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
local amount = m.sendmail_state.money
|
|
m.sendmail_state.sent_money = m.sendmail_state.money
|
|
m.sendmail_state.sent = false
|
|
|
|
if amount > 0 then
|
|
if not m.api.SendMailCODAllButton:GetChecked() then
|
|
m.sendmail_state.money = 0
|
|
end
|
|
if m.sendmail_state.cod then
|
|
m.sendmail_state.cod = amount
|
|
m.api.SetSendMailCOD( amount )
|
|
else
|
|
m.sendmail_state.money = 0
|
|
m.api.SetSendMailMoney( amount )
|
|
end
|
|
end
|
|
|
|
local subject = m.sendmail_state.subject
|
|
if subject == "" then
|
|
if item then
|
|
local item_name, texture, stack_count = m.api.GetSendMailItem()
|
|
subject = item_name .. (stack_count > 1 and " (" .. stack_count .. ")" or "")
|
|
m.sendmail_state.item = item_name
|
|
m.sendmail_state.icon = texture
|
|
else
|
|
subject = "<" .. m.api.NO_ATTACHMENTS .. ">"
|
|
end
|
|
elseif m.sendmail_state.numMessages > 1 then
|
|
subject = subject .. string.format( " [%d/%d]", m.sendmail_state.numMessages - getn( m.sendmail_state.attachments ),
|
|
m.sendmail_state.numMessages )
|
|
end
|
|
|
|
m.sendmail_state.sent_subject = subject
|
|
|
|
m.debug( "SendMail" )
|
|
m.api.SendMail( m.sendmail_state.to, subject, m.sendmail_state.body )
|
|
|
|
if getn( m.sendmail_state.attachments ) == 0 then
|
|
m.sendmail_sending = false
|
|
end
|
|
end
|
|
end
|