local StudioService = game:GetService("StudioService") local Rojo = script:FindFirstAncestor("Rojo") local Plugin = Rojo.Plugin local Packages = Rojo.Packages local Roact = require(Packages.Roact) local Flipper = require(Packages.Flipper) local Log = require(Packages.Log) local Theme = require(Plugin.App.Theme) local Assets = require(Plugin.Assets) local bindingUtil = require(Plugin.App.bindingUtil) local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync) local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local TextButton = require(Plugin.App.Components.TextButton) local baseClock = DateTime.now().UnixTimestampMillis local e = Roact.createElement local Notification = Roact.Component:extend("Notification") function Notification:init() self.motor = Flipper.SingleMotor.new(0) self.binding = bindingUtil.fromMotor(self.motor) self.lifetime = self.props.timeout self.motor:onStep(function(value) if value <= 0 and self.props.onClose then self.props.onClose() end end) end function Notification:dismiss() self.motor:setGoal(Flipper.Spring.new(0, { frequency = 5, dampingRatio = 1, })) end function Notification:didMount() self.motor:setGoal(Flipper.Spring.new(1, { frequency = 3, dampingRatio = 1, })) self.props.soundPlayer:play(Assets.Sounds.Notification) self.timeout = task.spawn(function() local clock = os.clock() local seen = false while task.wait(1 / 10) do local now = os.clock() local dt = now - clock clock = now if not seen then seen = StudioService.ActiveScript == nil end if not seen then -- Don't run down timer before being viewed continue end self.lifetime -= dt if self.lifetime <= 0 then self:dismiss() break end end end) end function Notification:willUnmount() task.cancel(self.timeout) end function Notification:render() local transparency = self.binding:map(function(value) return 1 - value end) return Theme.with(function(theme) local actionButtons = {} local buttonsX = 0 if self.props.actions then local count = 0 for key, action in self.props.actions do actionButtons[key] = e(TextButton, { text = action.text, style = action.style, onClick = function() local success, err = pcall(action.onClick, self) if not success then Log.warn("Error in notification action: " .. tostring(err)) end end, layoutOrder = -action.layoutOrder, transparency = transparency, }) buttonsX += getTextBoundsAsync(action.text, theme.Font.Main, theme.TextSize.Large, math.huge).X + (theme.TextSize.Body * 2) count += 1 end buttonsX += (count - 1) * 5 end local paddingY, logoSize = 20, 32 local actionsY = if self.props.actions then 35 else 0 local textXSpace = math.max(250, buttonsX) + 35 local textBounds = Vector2.new( textXSpace, getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Body, textXSpace).Y ) local contentX = math.max(textBounds.X, buttonsX) local size = self.binding:map(function(value) return UDim2.fromOffset( (35 + 40 + contentX) * value, 5 + actionsY + paddingY + math.max(logoSize, textBounds.Y) ) end) return e("TextButton", { BackgroundTransparency = 1, Size = size, LayoutOrder = self.props.layoutOrder, Text = "", ClipsDescendants = true, [Roact.Event.Activated] = function() self:dismiss() end, }, { e(BorderedContainer, { transparency = transparency, size = UDim2.new(1, 0, 1, 0), }, { Contents = e("Frame", { Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, }, { Logo = e("ImageLabel", { ImageTransparency = transparency, Image = Assets.Images.PluginButton, BackgroundTransparency = 1, Size = UDim2.new(0, logoSize, 0, logoSize), Position = UDim2.new(0, 0, 0, 0), AnchorPoint = Vector2.new(0, 0), }), Info = e("TextLabel", { Text = self.props.text, FontFace = theme.Font.Main, TextSize = theme.TextSize.Body, TextColor3 = theme.Notification.InfoColor, TextTransparency = transparency, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, TextWrapped = true, Size = UDim2.new(1, -35, 1, -actionsY), Position = UDim2.fromOffset(35, 0), LayoutOrder = 1, BackgroundTransparency = 1, }), Actions = if self.props.actions then e("Frame", { Size = UDim2.new(1, -40, 0, actionsY), Position = UDim2.new(1, 0, 1, 0), AnchorPoint = Vector2.new(1, 1), BackgroundTransparency = 1, }, { Layout = e("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, HorizontalAlignment = Enum.HorizontalAlignment.Right, VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder, Padding = UDim.new(0, 5), }), Buttons = Roact.createFragment(actionButtons), }) else nil, }), Padding = e("UIPadding", { PaddingLeft = UDim.new(0, 17), PaddingRight = UDim.new(0, 15), PaddingTop = UDim.new(0, paddingY / 2), PaddingBottom = UDim.new(0, paddingY / 2), }), }), }) end) end local Notifications = Roact.Component:extend("Notifications") function Notifications:render() local notifs = {} for id, notif in self.props.notifications do notifs["NotifID_" .. id] = e(Notification, { soundPlayer = self.props.soundPlayer, text = notif.text, timestamp = notif.timestamp, timeout = notif.timeout, actions = notif.actions, layoutOrder = (notif.timestamp - baseClock), onClose = function() self.props.onClose(id) end, }) end return Roact.createFragment(notifs) end return Notifications