local CoreGui = game:GetService("CoreGui") local RobloxGui = CoreGui:WaitForChild("RobloxGui") local Util = require(RobloxGui.Modules.Settings.Utility) local Panel3D = require(RobloxGui.Modules.VR.Panel3D) local VRHub = require(RobloxGui.Modules.VR.VRHub) local PANEL_OFFSET_CFRAME = CFrame.Angles(math.rad(-5), 0, 0) * CFrame.new(0, 4, 0) * CFrame.Angles(math.rad(-15), 0, 0) local NO_TRANSITION_ANIMATIONS = false local ANIMATE_OUT_DISTANCE = -100 local ANIMATE_OUT_DURATION = 0.25 local PIXELS_PER_STUD = 150 local WINDOW_TITLEBAR_HEIGHT = 72 local BLURRED_TITLEBAR_COLOR = Color3.new(78 / 255, 84 / 255, 96 / 255) local FOCUSED_TITLEBAR_COLOR = Color3.new(82 / 255, 101 / 255, 141 / 255) local WINDOW_BG_COLOR = Color3.new(20/255, 20/255, 20/255) local WINDOW_BG_TRANSPARENCY = 0.5 local POPOUT_DISTANCE = 0.25 local POPOUT_DURATION = 0.25 local NOTIFICATION_WIDTH_SCALE = 0.85 local NOTIFICATION_HEIGHT_OFFSET = 80 local NOTIFICATION_PADDING_Y = 20 local NOTIFICATION_PADDING_X_SCALE = (1 - NOTIFICATION_WIDTH_SCALE) / 2 local NOTIFICATION_DEPTH_OFFSET = 0.25 local NOTIFICATION_BG_COLOR = Color3.new(0.2, 0.2, 0.2) local NOTIFICATION_BG_TRANSPARENCY = 0.1 local MAX_NOTIFICATIONS_SHOWN = 3 local MAX_DETAILS_SHOWN = 2 local DETAILS_PADDING = 20 local BUTTON_1_POS = 0.07 local BUTTON_2_POS = 0.511 local BUTTON_SINGLE_SIZE = 0.86 local BUTTON_DOUBLE_SIZE = 0.415 local BUTTON_Y_POS = 0.55 local BUTTON_Y_SIZE = 0.29 local BUTTON_NORMAL_IMG = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png" local BUTTON_SELECTED_IMG = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png" local emptySelectionImage = Util:Create "ImageLabel" { BackgroundTransparency = 1, Image = "" } local AVATAR_IMAGE_URL = 'http://www.roblox.com/thumbs/avatar.ashx?userId=%d&x=%d&y=%d' local TEXT_COLOR = Color3.new(1, 1, 1) local aspectRatio = 1.62666666 local totalHeight = 3.5 local totalWidth = totalHeight * aspectRatio local leftPanelWidth = totalWidth * 0.4 local rightPanelWidth = totalWidth * 0.6 local panelOffset = totalWidth / 2 local leftOffset = panelOffset - (leftPanelWidth * 0.5) local rightOffset = leftOffset - (leftPanelWidth * 0.5) - (rightPanelWidth * 0.5) local NotificationHubModule = {} NotificationHubModule.ModuleName = "Notifications" NotificationHubModule.KeepVRTopbarOpen = true NotificationHubModule.VRIsExclusive = true NotificationHubModule.VRClosesNonExclusive = true NotificationHubModule.UnreadCountChanged = function() end VRHub:RegisterModule(NotificationHubModule) local notificationsPanel = Panel3D.Get("Notifications") local notificationsWindow = nil local detailsPanel = Panel3D.Get("NotificationDetails") local detailsWindow = nil local WindowFrame = {} do local windows = {} local WindowFrame_mt = { __index = WindowFrame } function WindowFrame.new(panel, parent, title) local instance = {} table.insert(windows, instance) instance.zeroCF = panel.localCF instance.zOffset = 0 instance.isPopping = false instance.isAnimating = false instance.tweener = nil instance.panel = panel instance.panel.OnMouseEnter = function() for i, v in pairs(windows) do if v ~= instance then v:SetPopOut(false) end end instance:SetPopOut(true) end instance.titlebar = Util:Create "ImageLabel" { Parent = parent, Position = UDim2.new(0, -1, 0, -1), Size = UDim2.new(1, 2, 0, WINDOW_TITLEBAR_HEIGHT + 2), BackgroundTransparency = 1, Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png", ImageColor3 = BLURRED_TITLEBAR_COLOR, ScaleType = Enum.ScaleType.Slice, SliceCenter = Rect.new(10, 10, 10, 10) } instance.titleText = Util:Create "TextLabel" { Parent = instance.titlebar, Position = UDim2.new(0, 1, 0, 1), Size = UDim2.new(1, -2, 1, -2), Text = title, TextColor3 = TEXT_COLOR, Font = Enum.Font.SourceSans, FontSize = Enum.FontSize.Size36, BackgroundTransparency = 1 } instance.content = Util:Create "ImageLabel" { Parent = parent, Position = UDim2.new(0, -1, 0, WINDOW_TITLEBAR_HEIGHT + 2), Size = UDim2.new(1, 2, 1, -WINDOW_TITLEBAR_HEIGHT - 4), BackgroundTransparency = 1, Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png", ImageColor3 = WINDOW_BG_COLOR, ImageTransparency = WINDOW_BG_TRANSPARENCY, ScaleType = Enum.ScaleType.Slice, SliceCenter = Rect.new(10, 10, 10, 10) } return setmetatable(instance, WindowFrame_mt) end function WindowFrame:SetTitle(title) self.titleText.Text = title end function WindowFrame:AddCloseButton(callback) local closeBtnSize = 48 local closeBtnOffset = 14 self.closeButton = Util:Create "ImageButton" { Parent = self.titlebar, Position = UDim2.new(0, closeBtnOffset, 0, closeBtnOffset), Size = UDim2.new(0, closeBtnSize, 0, closeBtnSize), BackgroundTransparency = 1, Image = "rbxasset://textures/ui/VR/closeButtonPadded.png" } self.closeButton.MouseButton1Click:connect(callback) end function WindowFrame:TweenZOffsetTo(zOffset, duration, easingFunc, callback) if self.tweener and not self.tweener:IsFinished() then self.tweener:Cancel() end self.tweener = Util:TweenProperty(self, "zOffset", self.zOffset, zOffset, duration, easingFunc, callback) end function WindowFrame:AnimateOut(callback) self.isAnimating = true self:TweenZOffsetTo(ANIMATE_OUT_DISTANCE, ANIMATE_OUT_DURATION, Util:GetEaseInOutQuad(), function() if callback then callback() end self.isAnimating = false end) end function WindowFrame:AnimateIn(callback) self.zOffset = ANIMATE_OUT_DISTANCE self:OnUpdate(0) self.isAnimating = true self:TweenZOffsetTo(0, ANIMATE_OUT_DURATION, Util:GetEaseInOutQuad(), function() if callback then callback() end self.isAnimating = false end) end function WindowFrame:SetPopOut(popOut) if self.isAnimating then return end if popOut then self.isPopping = true self:TweenZOffsetTo(POPOUT_DISTANCE, POPOUT_DURATION, Util:GetEaseInOutQuad(), function() self.isPopping = false end) else self.isPopping = true self:TweenZOffsetTo(0, POPOUT_DURATION, Util:GetEaseInOutQuad(), function() self.isPopping = false end) end end function WindowFrame:OnUpdate(dt) self.panel.localCF = self.zeroCF * CFrame.new(0, 0, -self.zOffset) if self.isPopping then local alpha = math.max(0, math.min(1, self.zOffset / POPOUT_DISTANCE)) self.titlebar.ImageColor3 = BLURRED_TITLEBAR_COLOR:lerp(FOCUSED_TITLEBAR_COLOR, alpha) end end end --Notifications panel setup do notificationsPanel:SetType(Panel3D.Type.Fixed) notificationsPanel:SetVisible(false) notificationsPanel:SetCanFade(false) notificationsPanel:ResizeStuds(leftPanelWidth, totalHeight, PIXELS_PER_STUD) local notificationsFrame = Util:Create "TextButton" { Parent = notificationsPanel:GetGUI(), Position = UDim2.new(0, 0, 0, 0), Size = UDim2.new(1, -4, 1, 0), BackgroundTransparency = 1, Text = "", Selectable = true, SelectionImageObject = emptySelectionImage } notificationsWindow = WindowFrame.new(notificationsPanel, notificationsFrame, "Notifications") notificationsWindow:AddCloseButton(function() NotificationHubModule:SetVisible(false) end) function notificationsPanel:OnUpdate(dt) notificationsWindow:OnUpdate(dt) end end --Details panel setup do detailsPanel:SetType(Panel3D.Type.Fixed) detailsPanel:SetVisible(false) detailsPanel:SetCanFade(false) detailsPanel:ResizeStuds(rightPanelWidth, totalHeight, PIXELS_PER_STUD) local detailsFrame = Util:Create "TextButton" { Parent = detailsPanel:GetGUI(), Position = UDim2.new(0, 0, 0, 0), Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, Text = "", Selectable = true, SelectionImageObject = emptySelectionImage } detailsWindow = WindowFrame.new(detailsPanel, detailsFrame, "Friend Requests") function detailsPanel:OnUpdate(dt) detailsWindow:OnUpdate(dt) end end local notificationsGroups = {} local notificationsGroupsList = {} local function groupSort(a, b) return a.order < b.order end local activeGroup = nil local function layoutNotificationsGroups() local y = NOTIFICATION_PADDING_Y for _, group in ipairs(notificationsGroupsList) do if #group.notifications > 0 then local height = NOTIFICATION_HEIGHT_OFFSET + (NOTIFICATION_PADDING_Y * (math.min(MAX_NOTIFICATIONS_SHOWN, #group.notifications) - 1)) local widthOffset = -((MAX_NOTIFICATIONS_SHOWN - 1) * NOTIFICATION_PADDING_Y) group.frame.Position = UDim2.new(NOTIFICATION_PADDING_X_SCALE, 0, 0, y) group.frame.Size = UDim2.new(NOTIFICATION_WIDTH_SCALE, widthOffset, 0, height) y = y + height + NOTIFICATION_PADDING_Y if group.notificationsDirty then group.notificationsDirty = false local notificationOffset = 0 local notificationDepth = 0 local notificationsEnd = #group.notifications if notificationsEnd > 0 then local notificationsStart = math.max(1, notificationsEnd - MAX_NOTIFICATIONS_SHOWN + 1) for i = 1, notificationsEnd do local notification = group.notifications[i] if i >= notificationsStart then notification.frame.Visible = true notification.frame.Position = UDim2.new(0, notificationOffset, 0, notificationOffset) notificationOffset = notificationOffset + NOTIFICATION_PADDING_Y local subpanel = notificationsPanel:SetSubpanelDepth(notification.frame, notificationDepth) notificationDepth = notificationDepth + NOTIFICATION_DEPTH_OFFSET else notification.frame.Visible = false end end if activeGroup == group then notificationsStart = math.max(1, notificationsEnd - MAX_DETAILS_SHOWN + 1) local detailY = 0 local fraction = 1 / MAX_DETAILS_SHOWN for i = 1, notificationsEnd do local notification = group.notifications[i] if i >= notificationsStart then notification.detailsFrame.Visible = true notification.detailsFrame.Position = UDim2.new(0, DETAILS_PADDING, detailY, DETAILS_PADDING) notification.detailsFrame.Size = UDim2.new(1, -DETAILS_PADDING * 2, fraction, -DETAILS_PADDING * 2) detailY = detailY + fraction else notification.detailsFrame.Visible = false end end end end end end end end local NotificationGroup = {} do local NotificationGroup_mt = { __index = NotificationGroup } function NotificationGroup.new(key, title, order) local self = setmetatable({}, NotificationGroup_mt) self.key = key self.title = title self.order = order self.notifications = {} self.notificationsDirty = false self.frame = Util:Create "Frame" { Parent = notificationsWindow.content, BackgroundTransparency = 1 } self.detailsFrame = Util:Create "Frame" { Parent = nil, BackgroundTransparency = 1, Position = UDim2.new(0, 0, 0, 0), Size = UDim2.new(1, 0, 1, 0) } notificationsGroups[key] = self table.insert(notificationsGroupsList, self) return self end function NotificationGroup:Deactivate() self.detailsFrame.Parent = nil for i, v in pairs(self.detailsFrame:GetChildren()) do detailsPanel:RemoveSubpanel(v) end end function NotificationGroup:SwitchTo() detailsWindow:SetTitle(self.title) for i, v in pairs(notificationsGroups) do if v ~= self then v:Deactivate() end end self.detailsFrame.Parent = detailsWindow.content activeGroup = self self.notificationsDirty = true end function NotificationGroup:BringNotificationToFront(notification) if activeGroup ~= self then self:SwitchTo() end if #self.notifications ~= 0 and notification == self.notifications[#self.notifications] then layoutNotificationsGroups() return --already on top, no point end for i, v in ipairs(self.notifications) do if v == notification then --take it out table.remove(self.notifications, i) break end end --put it back on top table.insert(self.notifications, notification) self.notificationsDirty = true layoutNotificationsGroups() end function NotificationGroup:RemoveNotification(notification) for i, v in ipairs(self.notifications) do if v == notification then table.remove(self.notifications, i) notificationsPanel:RemoveSubpanel(notification.frame) detailsPanel:RemoveSubpanel(notification.detailsFrame) notification.detailsFrame:Destroy() notification.frame:Destroy() self.notificationsDirty = true layoutNotificationsGroups() return end end end function NotificationGroup:GetTopNotification() local numNotifications = #self.notifications if numNotifications <= 0 then return nil end return self.notifications[numNotifications] end end NotificationGroup.new("Friends", "Friends", 1) NotificationGroup.new("BadgeAwards", "Badges", 2) NotificationGroup.new("PlayerPoints", "Points", 3) NotificationGroup.new("Developer", "Other", 4) table.sort(notificationsGroupsList, groupSort) local function doCallback(callback, ...) if not callback then return end if type(callback) == "function" then callback(...) return end if callback:IsA("BindableEvent") then callback:Fire(...) return end if callback:IsA("BindableFunction") then callback:Invoke(...) return end end local Notification = {} do local Notification_mt = { __index = Notification } function Notification.new(group, notificationInfo) local self = setmetatable({}, Notification_mt) self.group = group self.frame = Util:Create "ImageButton" { Parent = group.frame, Size = UDim2.new(1, 0, 0, NOTIFICATION_HEIGHT_OFFSET), SelectionImageObject = emptySelectionImage, BackgroundTransparency = 1 --when we have proper frame rendering with AA, we can change this and remove the stand-in background } self.frame.MouseButton1Click:connect(function() self:OnClicked() end) self.background = Util:Create "ImageLabel" { --this is the stand-in background for that smoooooooth edge rendering Parent = self.frame, Position = UDim2.new(0, -1, 0, -1), Size = UDim2.new(1, 2, 1, 2), BackgroundTransparency = 1, Image = "rbxasset://textures/ui/vr/rectBackgroundWhite.png", ImageColor3 = NOTIFICATION_BG_COLOR, ImageTransparency = NOTIFICATION_BG_TRANSPARENCY, ScaleType = Enum.ScaleType.Slice, SliceCenter = Rect.new(10, 10, 10, 10) } self.imageBackground = Util:Create "ImageLabel" { Parent = self.frame, Position = UDim2.new(0, 5, 0, 5), Size = UDim2.new(0, 70, 0, 70), BackgroundTransparency = 1, Image = "rbxasset://textures/ui/VR/circleWhite.png", ImageColor3 = notificationInfo.imgBackgroundColor or Color3.new(1, 1, 1) } self.image = Util:Create "ImageLabel" { Parent = self.imageBackground, Position = UDim2.new(0, 0, 0, 0), Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, Image = notificationInfo.Image } local text = notificationInfo.Text if notificationInfo.Title and notificationInfo.Text then text = ("%s\n%s"):format(notificationInfo.Title, notificationInfo.Text) end self.text = Util:Create "TextLabel" { Parent = self.frame, Position = UDim2.new(0, NOTIFICATION_HEIGHT_OFFSET, 0, 0), Size = UDim2.new(1, -NOTIFICATION_HEIGHT_OFFSET, 1, 0), BackgroundTransparency = 1, TextXAlignment = Enum.TextXAlignment.Left, Text = text, Font = Enum.Font.SourceSans, FontSize = Enum.FontSize.Size18, TextColor3 = TEXT_COLOR } self.detailsFrame = Util:Create "Frame" { Parent = group.detailsFrame, BackgroundTransparency = 1 } self.detailsFrame.MouseEnter:connect(function() detailsPanel:SetSubpanelDepth(self.detailsFrame, 0.25) end) self.detailsFrame.MouseLeave:connect(function() detailsPanel:SetSubpanelDepth(self.detailsFrame, 0) end) self.detailsBackground = Util:Create "ImageLabel" { Parent = self.detailsFrame, Position = UDim2.new(0, -1, 0, -1), Size = UDim2.new(1, 2, 1, 2), BackgroundTransparency = 1, Image = "rbxasset://textures/ui/VR/rectBackgroundWhite.png", ImageColor3 = Color3.new(0.2, 0.2, 0.2), ImageTransparency = 0.1, ScaleType = Enum.ScaleType.Slice, SliceCenter = Rect.new(10,10,10,10) } self.detailsIconBackground = Util:Create "ImageLabel" { Parent = self.detailsFrame, Position = UDim2.new(0, 20, 0, 10), Size = UDim2.new(0, 80, 0, 80), BackgroundTransparency = 1, Image = "rbxasset://textures/ui/VR/circleWhite.png", ImageColor3 = notificationInfo.imgBackgroundColor or Color3.new(1, 1, 1) } self.detailsIcon = Util:Create "ImageLabel" { Parent = self.detailsIconBackground, Position = UDim2.new(0, 0, 0, 0), Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, Image = notificationInfo.Image } local detailText = notificationInfo.DetailText or notificationInfo.Title self.detailsText = Util:Create "TextLabel" { Parent = self.detailsFrame, Position = UDim2.new(0, 110, 0, 10), Size = UDim2.new(1, -120, 0, 90), BackgroundTransparency = 1, Text = detailText, TextColor3 = TEXT_COLOR, TextXAlignment = Enum.TextXAlignment.Left, Font = Enum.Font.SourceSansBold, FontSize = Enum.FontSize.Size36 } local function createButton(xPosScale, xSizeScale, text) local button, text = Util:Create "ImageButton" { Parent = self.detailsFrame, Position = UDim2.new(xPosScale, 0, BUTTON_Y_POS, 0), Size = UDim2.new(xSizeScale, 0, BUTTON_Y_SIZE, 0), BackgroundTransparency = 1, Image = BUTTON_NORMAL_IMG, ScaleType = Enum.ScaleType.Slice, SliceCenter = Rect.new(8,6,46,44), SelectionImageObject = emptySelectionImage }, Util:Create "TextLabel" { Position = UDim2.new(0, 0, 0, 0), Size = UDim2.new(1, 0, 1, -6), BackgroundTransparency = 1, Text = text, TextColor3 = TEXT_COLOR, Font = Enum.Font.SourceSansBold, FontSize = Enum.FontSize.Size24 } text.Parent = button button.SelectionGained:connect(function() button.Image = BUTTON_SELECTED_IMG end) button.SelectionLost:connect(function() button.Image = BUTTON_NORMAL_IMG end) return button, text end if notificationInfo.Button1Text and notificationInfo.Button2Text then self.detailsButton1, self.detailsButton1Text = createButton(BUTTON_1_POS, BUTTON_DOUBLE_SIZE, notificationInfo.Button1Text) self.detailsButton2, self.detailsButton2Text = createButton(BUTTON_2_POS, BUTTON_DOUBLE_SIZE, notificationInfo.Button2Text) self.detailsButton1.MouseButton1Click:connect(function() doCallback(notificationInfo.Callback, notificationInfo.Button1Text) self:Dismiss() end) self.detailsButton2.MouseButton1Click:connect(function() doCallback(notificationInfo.Callback, notificationInfo.Button2Text) self:Dismiss() end) elseif not notificationInfo.button2Text then local text = notificationInfo.Button1Text or "Dismiss" self.detailsButton1, self.detailsButton1Text = createButton(BUTTON_1_POS, BUTTON_SINGLE_SIZE, text) self.detailsButton1.MouseButton1Click:connect(function() doCallback(notificationInfo.Callback, notificationInfo.Button1Text) self:Dismiss() end) end table.insert(group.notifications, self) group.notificationsDirty = true layoutNotificationsGroups() return self end function Notification:OnClicked() self.group:BringNotificationToFront(self) end function Notification:Dismiss() self.group:RemoveNotification(self) end end --NotificationHubModule API and state management do local pendingNotifications = {} local isVisible = false local unreadCount = 0 local SendNotificationInfoEvent = RobloxGui:WaitForChild("SendNotificationInfo") SendNotificationInfoEvent.Event:connect(function(notificationInfo) local group = notificationsGroups[notificationInfo.GroupName or "Developer"] if not group then group = notificationsGroups.Developer end Notification.new(group, notificationInfo) if not isVisible then unreadCount = unreadCount + 1 NotificationHubModule.UnreadCountChanged(unreadCount) end end) NotificationHubModule.VisibilityStateChanged = Util:Create "BindableEvent" { Name = "VisibilityStateChanged" } function NotificationHubModule:GetNumberOfPendingNotifications() return #pendingNotifications end function NotificationHubModule:IsVisible() return isVisible end function NotificationHubModule:SetVisible(visible) if isVisible == visible then return end isVisible = visible local topbarPanel = Panel3D.Get("Topbar3D") topbarPanel:SetCanFade(not visible) if visible then unreadCount = 0 NotificationHubModule.UnreadCountChanged(unreadCount) local hubSpace = topbarPanel.localCF * PANEL_OFFSET_CFRAME notificationsPanel.localCF = hubSpace * CFrame.new(leftOffset, 0, 0) notificationsWindow.zeroCF = notificationsPanel.localCF if not NO_TRANSITION_ANIMATIONS then notificationsWindow:AnimateIn(nil) end detailsPanel.localCF = hubSpace * CFrame.new(rightOffset, 0, 0) detailsWindow.zeroCF = detailsPanel.localCF if not NO_TRANSITION_ANIMATIONS then detailsWindow:AnimateIn(nil) end notificationsPanel:SetVisible(true) detailsPanel:SetVisible(true) VRHub:FireModuleOpened(NotificationHubModule.ModuleName) else if not NO_TRANSITION_ANIMATIONS then spawn(function() cancelAnimation = false notificationsWindow:AnimateOut(function() notificationsPanel:SetVisible(false) end) detailsWindow:AnimateOut(function() detailsPanel:SetVisible(false) end) end) else notificationsPanel:SetVisible(false) detailsPanel:SetVisible(false) end VRHub:FireModuleClosed(NotificationHubModule.ModuleName) end NotificationHubModule.VisibilityStateChanged:Fire(visible) end VRHub.ModuleOpened.Event:connect(function(moduleName, isExclusive, shouldCloseNonExclusive, shouldKeepTopbarOpen) if moduleName ~= NotificationHubModule.ModuleName then NotificationHubModule:SetVisible(false) end end) end return NotificationHubModule