local _G = ShaguTweaks.GetGlobalEnv() local T = ShaguTweaks.T local API = ShaguTweaks.API or {} local rgbhex = ShaguTweaks.rgbhex local strsplit = ShaguTweaks.strsplit local scrollspeed = 1 local module = ShaguTweaks:register({ title = T["Chat Tweaks"], description = T["Improves chat with mouse wheel scrolling, sticky channels, arrow-up repeat, shortened names, item link preview, Alt/Ctrl-click to invite/target, chat history, and clickable URLs."], expansions = { ["vanilla"] = true }, category = T["Chat & Social"], enabled = true, order = 60, }) -- ============================================================ -- URL pattern helpers (from chat-links) -- ============================================================ local URLPattern = { WWW = { ["rx"]=" (www%d-)%.([_A-Za-z0-9-]+)%.(%S+)%s?", ["fm"]="%s.%s.%s" }, PROTOCOL = { ["rx"]=" (%a+)://(%S+)%s?", ["fm"]="%s://%s" }, EMAIL = { ["rx"]=" ([_A-Za-z0-9-%.:]+)@([_A-Za-z0-9-]+)(%.)([_A-Za-z0-9-]+%.?[_A-Za-z0-9-]*)%s?", ["fm"]="%s@%s%s%s" }, PORTIP = { ["rx"]=" (%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?):(%d%d?%d?%d?%d?)%s?", ["fm"]="%s.%s.%s.%s:%s" }, IP = { ["rx"]=" (%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%s?", ["fm"]="%s.%s.%s.%s" }, SHORTURL = { ["rx"]=" (%a+)%.(%a+)/(%S+)%s?", ["fm"]="%s.%s/%s" }, URLIP = { ["rx"]=" ([_A-Za-z0-9-]+)%.([_A-Za-z0-9-]+)%.(%S+)%:([_0-9-]+)%s?", ["fm"]="%s.%s.%s:%s" }, URL = { ["rx"]=" ([_A-Za-z0-9-]+)%.([A-Za-z][A-Za-z]+)(%S*)%s?", ["fm"]="%s.%s%s" }, } -- Keep naked-domain detection conservative: explicit protocols and e-mail -- addresses keep their historical behavior, while plain domains are linked -- only when their final label is a known TLD. The list is built once at load. local KnownTLD = {} local KnownTLDList = "ac ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo bq br bs bt bv bw by bz " .. "ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg eh er es et eu fi fj fk fm fo fr " .. "ga gb gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp " .. "ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz " .. "na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sj sk sl sm sn so sr ss st su sv sx sy sz " .. "tc td tf tg th tj tk tl tm tn to tr tt tv tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw " .. "aero app asia biz blog cat club cloud com coop dev digital edu email games game gov info int jobs link live media mil mobi museum name net news online org pro shop site social space store support tech tel top travel vip website wiki world xyz" for tld in string.gfind(KnownTLDList, "%S+") do KnownTLD[tld] = true end KnownTLDList = nil local function IsKnownTLD(value) if not value then return end local _, _, tld = string.find(value, "^([A-Za-z][A-Za-z]+)") if not tld then return end return KnownTLD[string.lower(tld)] end local function FormatLink(formatter,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) if not (formatter and a1) then return end local newtext = string.format(formatter,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) local invalidtld for _, arg in pairs({a10,a9,a8,a7,a6,a5,a4,a3,a2,a1}) do if arg then invalidtld = string.find(arg, "(%.%.)$") break end end if (invalidtld) then return newtext end if formatter == URLPattern.EMAIL.fm then local colon = string.find(a1,":") if (colon) and string.len(a1) > colon then if not (string.sub(a1,1,6) == "mailto") then local prefix,address = string.sub(newtext,1,colon),string.sub(newtext,colon+1) return string.format(" %s|cffccccff|Hurl:%s|h[%s]|h|r ",prefix,address,address) end end end return " |cffccccff|Hurl:" .. newtext .. "|h[" .. newtext .. "]|h|r " end local URLFuncs = { ["WWW"] = function(a1,a2,a3) if IsKnownTLD(a3) then return FormatLink(URLPattern.WWW.fm,a1,a2,a3) end end, ["PROTOCOL"] = function(a1,a2) return FormatLink(URLPattern.PROTOCOL.fm,a1,a2) end, ["EMAIL"] = function(a1,a2,a3,a4) return FormatLink(URLPattern.EMAIL.fm,a1,a2,a3,a4) end, ["PORTIP"] = function(a1,a2,a3,a4,a5) return FormatLink(URLPattern.PORTIP.fm,a1,a2,a3,a4,a5) end, ["IP"] = function(a1,a2,a3,a4) return FormatLink(URLPattern.IP.fm,a1,a2,a3,a4) end, ["SHORTURL"] = function(a1,a2,a3) if IsKnownTLD(a2) then return FormatLink(URLPattern.SHORTURL.fm,a1,a2,a3) end end, ["URLIP"] = function(a1,a2,a3,a4) if IsKnownTLD(a3) then return FormatLink(URLPattern.URLIP.fm,a1,a2,a3,a4) end end, ["URL"] = function(a1,a2,a3) if IsKnownTLD(a2) then return FormatLink(URLPattern.URL.fm,a1,a2,a3) end end, } local function MayContainURL(text) if string.find(text, "://", 1, true) or string.find(text, "@", 1, true) then return true end if not string.find(text, ".", 1, true) then return false end -- Fast path for numeric IP-style candidates. if string.find(text, "%d+%.%d+") then return true end -- A normal sentence ending with punctuation should not run all URL regexes. -- Only dotted alphabetic labels with a known TLD qualify as a candidate. for tld in string.gfind(text, "%.([A-Za-z][A-Za-z]+)") do if KnownTLD[string.lower(tld)] then return true end end return false end local function HandleLink(text) text = string.gsub(text, URLPattern.WWW.rx, URLFuncs.WWW) text = string.gsub(text, URLPattern.PROTOCOL.rx, URLFuncs.PROTOCOL) text = string.gsub(text, URLPattern.EMAIL.rx, URLFuncs.EMAIL) text = string.gsub(text, URLPattern.PORTIP.rx, URLFuncs.PORTIP) text = string.gsub(text, URLPattern.IP.rx, URLFuncs.IP) text = string.gsub(text, URLPattern.SHORTURL.rx, URLFuncs.SHORTURL) text = string.gsub(text, URLPattern.URLIP.rx, URLFuncs.URLIP) text = string.gsub(text, URLPattern.URL.rx, URLFuncs.URL) return text end -- URL copy dialog local CopyLinkDialog = CreateFrame("Frame", "ShaguTweaksURLCopy", UIParent) CopyLinkDialog:Hide() CopyLinkDialog:SetWidth(300) CopyLinkDialog:SetHeight(90) CopyLinkDialog:SetFrameStrata("FULLSCREEN") CopyLinkDialog:SetPoint("CENTER", 0, 0) CopyLinkDialog:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32, insets = { left = 11, right = 12, top = 12, bottom = 11 } }) CopyLinkDialog:SetScript("OnShow", function() this.text:HighlightText() end) CopyLinkDialog.text = CreateFrame("EditBox", "ShaguTweaksURLCopyEditBox", CopyLinkDialog) CopyLinkDialog.text:SetTextColor(1,.8,0) CopyLinkDialog.text:SetJustifyH("CENTER") CopyLinkDialog.text:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) CopyLinkDialog.text:SetBackdropColor(0,0,0,.8) CopyLinkDialog.text:SetBackdropBorderColor(.8,.8,.8,1) CopyLinkDialog.text:SetWidth(260) CopyLinkDialog.text:SetHeight(25) CopyLinkDialog.text:SetPoint("TOP", CopyLinkDialog, "TOP", 0, -20) CopyLinkDialog.text:SetFontObject(GameFontNormal) CopyLinkDialog.text:SetScript("OnEscapePressed", function() CopyLinkDialog:Hide() end) CopyLinkDialog.text:SetScript("OnEditFocusLost", function() CopyLinkDialog:Hide() end) CopyLinkDialog.close = CreateFrame("Button", "ShaguTweaksURLCopyClose", CopyLinkDialog, "UIPanelButtonTemplate") CopyLinkDialog.close:SetPoint("BOTTOMRIGHT", CopyLinkDialog, "BOTTOMRIGHT", -20, 20) CopyLinkDialog.close:SetWidth(70) CopyLinkDialog.close:SetHeight(18) CopyLinkDialog.close:SetText(CLOSE) CopyLinkDialog.close:SetScript("OnClick", function() CopyLinkDialog:Hide() end) CopyLinkDialog.CopyText = function(text) CopyLinkDialog.text:SetText(text) CopyLinkDialog:Show() end -- ============================================================ -- Module enable -- ============================================================ module.enable = function(self) -- -------------------------------------------------------- -- Sticky channels & arrow-up repeat -- -------------------------------------------------------- ChatTypeInfo.WHISPER.sticky = 1 ChatTypeInfo.OFFICER.sticky = 1 ChatTypeInfo.RAID_WARNING.sticky = 1 ChatTypeInfo.CHANNEL.sticky = 1 ChatFrameEditBox:SetAltArrowKeyMode(false) -- -------------------------------------------------------- -- Mouse wheel scrolling -- -------------------------------------------------------- local function ChatOnMouseWheel() if arg1 > 0 then if API.IsShiftKeyDown and API.IsShiftKeyDown() then this:ScrollToTop() else for i=1,scrollspeed do this:ScrollUp() end end elseif arg1 < 0 then if API.IsShiftKeyDown and API.IsShiftKeyDown() then this:ScrollToBottom() else for i=1,scrollspeed do this:ScrollDown() end end end if this:AtBottom() then _G[this:GetName().."BottomButton"]:Hide() this.scroll = nil else this.scroll = true _G[this:GetName().."BottomButton"]:Show() end end for i=1, NUM_CHAT_WINDOWS do _G["ChatFrame"..i]:EnableMouseWheel(true) _G["ChatFrame"..i].scroll = nil _G["ChatFrame"..i]:SetScript("OnMouseWheel", ChatOnMouseWheel) end -- -------------------------------------------------------- -- Chat history save/restore -- -------------------------------------------------------- local realm = GetRealmName() local player = UnitName("player") ShaguTweaks_cache = ShaguTweaks_cache or {} ShaguTweaks_cache["chathistory"] = ShaguTweaks_cache["chathistory"] or {} ShaguTweaks_cache["chathistory"][realm] = ShaguTweaks_cache["chathistory"][realm] or {} ShaguTweaks_cache["chathistory"][realm][player] = ShaguTweaks_cache["chathistory"][realm][player] or {} local playerHistory = ShaguTweaks_cache["chathistory"][realm][player] local function GetChatHistory(id) if not playerHistory[id] then playerHistory[id] = {} end return playerHistory[id] end local function SaveChatHistory(id, msg, r, g, b) if r and g and b then local color = rgbhex(r*.5+.2, g*.5+.2, b*.5+.2) msg = color .. msg if string.find(msg, "|r", 1, true) then msg = string.gsub(msg, "|r", "|r"..color) end end local history = GetChatHistory(id) table.insert(history, 1, msg) if history[30] then table.remove(history, 30) end end -- -------------------------------------------------------- -- ClassicAPI quest-link tooltip -- -------------------------------------------------------- local QuestLog = _G.C_QuestLog local EventUtils = _G.C_EventUtils local hasQuestDetails = type(QuestLog) == "table" and type(QuestLog.GetQuestDetails) == "function" local canCheckQuestStatus = hasQuestDetails and type(QuestLog.IsOnQuest) == "function" local canLoadQuest = hasQuestDetails and type(QuestLog.RequestLoadQuestByID) == "function" and type(EventUtils) == "table" and type(EventUtils.IsEventValid) == "function" and EventUtils.IsEventValid("QUEST_DATA_LOAD_RESULT") local function CleanLinkText(text) if not text then return end text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") text = string.gsub(text, "|r", "") text = string.gsub(text, "|H.-|h(.-)|h", "%1") text = string.gsub(text, "^%[(.*)%]$", "%1") return text end local function ShowQuestTooltip(owner, questID, fallbackTitle) if not hasQuestDetails then return end local details = QuestLog.GetQuestDetails(questID) if not details then return end local title = details.title if not title or title == "" then title = fallbackTitle or ("Quest " .. questID) end owner.ShaguTweaksQuestTooltip = true GameTooltip:SetOwner(owner, "ANCHOR_CURSOR") GameTooltip:ClearLines() GameTooltip:AddLine(title, 1, .82, 0) if canCheckQuestStatus and QuestLog.IsOnQuest(questID) then GameTooltip:AddLine("You are on this quest.", .2, 1, .2) end local info if details.level and details.level > 0 then info = (_G.LEVEL or "Level") .. " " .. details.level end if details.questType and details.questType ~= "" then info = info and (info .. " - " .. details.questType) or details.questType end if info then GameTooltip:AddLine(info, .8, .8, .8) end if details.objectives and details.objectives ~= "" then GameTooltip:AddLine(" ") GameTooltip:AddLine(details.objectives, 1, 1, 1, true) end GameTooltip:Show() return true end if canLoadQuest and not self.questTooltipLoader then self.questTooltipLoader = CreateFrame("Frame") end local questTooltipLoader = canLoadQuest and self.questTooltipLoader local function ClearPendingQuest(owner) if not questTooltipLoader then return end if owner and questTooltipLoader.owner ~= owner then return end questTooltipLoader:UnregisterEvent("QUEST_DATA_LOAD_RESULT") questTooltipLoader.owner = nil questTooltipLoader.questID = nil questTooltipLoader.title = nil end if questTooltipLoader then questTooltipLoader:SetScript("OnEvent", function() if event ~= "QUEST_DATA_LOAD_RESULT" or tonumber(arg1) ~= this.questID then return end local owner = this.owner local questID = this.questID local title = this.title ClearPendingQuest() if arg2 and owner and owner.ShaguTweaksQuestTooltip then ShowQuestTooltip(owner, questID, title) end end) end -- -------------------------------------------------------- -- Per-frame hooks: history + CLINK cleanup + URLs -- -------------------------------------------------------- for i=1, NUM_CHAT_WINDOWS do local isCombat = 0 if _G["ChatFrame"..i] and _G["ChatFrame"..i].messageTypeList then for _, msg in pairs(_G["ChatFrame"..i].messageTypeList) do if strfind(msg,"SPELL",1) or strfind(msg,"COMBAT",1) then isCombat = isCombat + 1 end end end if isCombat <= 5 and _G["ChatFrame"..i] and not _G["ChatFrame"..i].ShaguTweaksChatTweaksHooked then _G["ChatFrame"..i].ShaguTweaksChatTweaksHooked = true -- restore history local history = GetChatHistory(i) for j=30,0,-1 do if history[j] then _G["ChatFrame"..i]:AddMessage(history[j], .7,.7,.7) end end _G["ChatFrame"..i].ShaguTweaksBaseAddMessage = _G["ChatFrame"..i].AddMessage _G["ChatFrame"..i].AddMessage = function(frame, text, a1, a2, a3, a4, a5) if not text then return end -- Remove prat/chatter CLINKs only when the marker is actually present. if string.find(text, "{CLINK:", 1, true) then text = gsub(text, "{CLINK:(%x+):([%d-]-:[%d-]-:[%d-]-:[%d-]-:[%d-]-:[%d-]-:[%d-]-:[%d-]-):([^}]-)}", "|c%1|Hitem:%2|h[%3]|h|r") text = gsub(text, "{CLINK:(%x+):([%d-]-:[%d-]-:[%d-]-:[%d-]-):([^}]-)}", "|c%1|Hitem:%2|h[%3]|h|r") text = gsub(text, "{CLINK:item:(%x+):([%d-]-:[%d-]-:[%d-]-:[%d-]-:[%d-]-:[%d-]-:[%d-]-:[%d-]-):([^}]-)}", "|c%1|Hitem:%2|h[%3]|h|r") text = gsub(text, "{CLINK:enchant:(%x+):([%d-]-):([^}]-)}", "|c%1|Henchant:%2|h[%3]|h|r") text = gsub(text, "{CLINK:spell:(%x+):([%d-]-):([^}]-)}", "|c%1|Hspell:%2|h[%3]|h|r") text = gsub(text, "{CLINK:quest:(%x+):([%d-]-):([%d-]-):([^}]-)}", "|c%1|Hquest:%2:%3|h[%4]|h|r") end -- Reduce channel number (e.g. [1. General] -> [1]). This transform -- can only match lines that already contain a player hyperlink. if string.find(text, "|Hplayer", 1, true) then local channel = string.gsub(text, ".*%[(.-)%]%s+(.*|Hplayer).+", "%1") if string.find(channel, "%d+%. ") then channel = string.gsub(channel, "(%d+)%..*", "%1") text = string.gsub(text, "%[%d+%..-%]%s+(.*|Hplayer)", "[" .. channel .. "] %1") end end -- Skip all expensive URL patterns unless a real candidate is present. if MayContainURL(text) then text = HandleLink(text) end SaveChatHistory(frame:GetID(), text, a1, a2, a3) frame:ShaguTweaksBaseAddMessage(text, a1, a2, a3, a4, a5) end local chatFrame = _G["ChatFrame"..i] local baseHyperlinkEnter = chatFrame:GetScript("OnHyperlinkEnter") local baseHyperlinkLeave = chatFrame:GetScript("OnHyperlinkLeave") local baseHyperlinkClick = chatFrame:GetScript("OnHyperlinkClick") -- Add item and quest previews without discarding Turtle or addon handlers. chatFrame:SetScript("OnHyperlinkEnter", function() local _, _, linktype = string.find(arg1, "^(.-):(.+)$") if linktype == "item" then ClearPendingQuest(this) this.ShaguTweaksItemTooltip = true GameTooltip:SetOwner(this, "ANCHOR_CURSOR") GameTooltip:SetHyperlink(arg1) GameTooltip:Show() elseif linktype == "quest" and hasQuestDetails then local _, _, questID = string.find(arg1, "^quest:(%d+)") questID = tonumber(questID) if questID then local title = CleanLinkText(arg2) ClearPendingQuest(this) if ShowQuestTooltip(this, questID, title) then return end if questTooltipLoader then this.ShaguTweaksQuestTooltip = true GameTooltip:SetOwner(this, "ANCHOR_CURSOR") GameTooltip:ClearLines() GameTooltip:AddLine(title or ("Quest " .. questID), 1, .82, 0) GameTooltip:Show() questTooltipLoader.owner = this questTooltipLoader.questID = questID questTooltipLoader.title = title questTooltipLoader:RegisterEvent("QUEST_DATA_LOAD_RESULT") QuestLog.RequestLoadQuestByID(questID) return end end if baseHyperlinkEnter then baseHyperlinkEnter() end elseif baseHyperlinkEnter then baseHyperlinkEnter() end end) chatFrame:SetScript("OnHyperlinkLeave", function() if this.ShaguTweaksItemTooltip or this.ShaguTweaksQuestTooltip then this.ShaguTweaksItemTooltip = nil this.ShaguTweaksQuestTooltip = nil ClearPendingQuest(this) GameTooltip:Hide() elseif baseHyperlinkLeave then baseHyperlinkLeave() end end) -- Keep Chat Tweaks-specific actions local to the chat frame. Other -- hyperlink types stay owned by the native Turtle/addon handler. chatFrame:SetScript("OnHyperlinkClick", function() local _, _, playerLink = string.find(arg1, "(player:.+)") if playerLink then local _, pname = strsplit(":", playerLink) if API.IsAltKeyDown and API.IsAltKeyDown() then InviteByName(pname) return elseif API.IsControlKeyDown and API.IsControlKeyDown() then TargetByName(pname, true) return elseif API.IsShiftKeyDown and API.IsShiftKeyDown() and ChatFrameEditBox:IsVisible() then ChatFrameEditBox:Insert("|cffffffff|Hplayer:"..pname.."|h["..pname.."]|h|r") return end elseif strsub(arg1,1,3) == "url" then if string.len(arg1) > 4 and string.sub(arg1,1,4) == "url:" then CopyLinkDialog.CopyText(string.sub(arg1, 5)) end return end if baseHyperlinkClick then baseHyperlinkClick() else ChatFrame_OnHyperlinkShow(arg1, arg2, arg3) end end) end end -- -------------------------------------------------------- -- Shorten channel names -- -------------------------------------------------------- local left, right = "[", "]" local default = " " .. "%s" .. "|r:" .. "\32" _G.CHAT_CHANNEL_GET = "%s" .. "|r:" .. "\32" _G.CHAT_GUILD_GET = left.."G"..right .. default _G.CHAT_OFFICER_GET = left.."O"..right .. default _G.CHAT_PARTY_GET = left.."P"..right .. default _G.CHAT_RAID_GET = left.."R"..right .. default _G.CHAT_RAID_LEADER_GET = left.."RL"..right .. default _G.CHAT_RAID_WARNING_GET = left.."RW"..right .. default _G.CHAT_BATTLEGROUND_GET = left.."BG"..right .. default _G.CHAT_BATTLEGROUND_LEADER_GET = left.."BL"..right .. default _G.CHAT_SAY_GET = left.."S"..right .. default _G.CHAT_YELL_GET = left.."Y"..right .. default _G.CHAT_WHISPER_GET = "[From]" .. default _G.CHAT_WHISPER_INFORM_GET = "[To]" .. default end