mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 12:45:05 +00:00
Add notification popups (#540)
* Add notifications prototype * Add timeout * Improve function name * Faster timeouts and fully clickable * Update remove padding from old X button * Only auto-dismiss when viewport is open * Start auto dismiss once viewed * Avoid redundantly displaying widget text as notifs * Add sound effect * Add setting for notifications * Remove duplicate PluginSettings.StudioProvider * Use short pop sound effect * Fix broken audio, thanks Roblox * Use e instead of createElement
This commit is contained in:
BIN
assets/NotificationPop.mp3
Normal file
BIN
assets/NotificationPop.mp3
Normal file
Binary file not shown.
198
plugin/src/App/Notifications.lua
Normal file
198
plugin/src/App/Notifications.lua
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
local TextService = game:GetService("TextService")
|
||||||
|
local StudioService = game:GetService("StudioService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
|
||||||
|
local Roact = require(Rojo.Roact)
|
||||||
|
local Flipper = require(Rojo.Flipper)
|
||||||
|
|
||||||
|
local bindingUtil = require(script.Parent.bindingUtil)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local playSound = require(Plugin.playSound)
|
||||||
|
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
|
||||||
|
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 then
|
||||||
|
if self.props.onClose then
|
||||||
|
self.props.onClose()
|
||||||
|
end
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
playSound(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 time = DateTime.fromUnixTimestampMillis(self.props.timestamp)
|
||||||
|
|
||||||
|
local textBounds = TextService:GetTextSize(
|
||||||
|
self.props.text,
|
||||||
|
15,
|
||||||
|
Enum.Font.GothamSemibold,
|
||||||
|
Vector2.new(350, 700)
|
||||||
|
)
|
||||||
|
|
||||||
|
local transparency = self.binding:map(function(value)
|
||||||
|
return 1 - value
|
||||||
|
end)
|
||||||
|
|
||||||
|
local size = self.binding:map(function(value)
|
||||||
|
return UDim2.fromOffset(
|
||||||
|
(35+40+textBounds.X)*value,
|
||||||
|
math.max(14+20+textBounds.Y, 32+20)
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
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),
|
||||||
|
}, {
|
||||||
|
TextContainer = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 35+textBounds.X, 1, -20),
|
||||||
|
Position = UDim2.new(0, 0, 0, 10),
|
||||||
|
BackgroundTransparency = 1
|
||||||
|
}, {
|
||||||
|
Logo = e("ImageLabel", {
|
||||||
|
ImageTransparency = transparency,
|
||||||
|
Image = Assets.Images.PluginButton,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 32, 0, 32),
|
||||||
|
Position = UDim2.new(0, 0, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
}),
|
||||||
|
Info = e("TextLabel", {
|
||||||
|
Text = self.props.text,
|
||||||
|
Font = Enum.Font.GothamSemibold,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = theme.Notification.InfoColor,
|
||||||
|
TextTransparency = transparency,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextWrapped = true,
|
||||||
|
|
||||||
|
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
|
||||||
|
Position = UDim2.fromOffset(35, 0),
|
||||||
|
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
Time = e("TextLabel", {
|
||||||
|
Text = time:FormatLocalTime("LTS", "en-us"),
|
||||||
|
Font = Enum.Font.Code,
|
||||||
|
TextSize = 12,
|
||||||
|
TextColor3 = theme.Notification.InfoColor,
|
||||||
|
TextTransparency = transparency,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
|
||||||
|
Size = UDim2.new(1, -35, 0, 14),
|
||||||
|
Position = UDim2.new(0, 35, 1, -14),
|
||||||
|
|
||||||
|
LayoutOrder = 1,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 17),
|
||||||
|
PaddingRight = UDim.new(0, 15),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local Notifications = Roact.Component:extend("Notifications")
|
||||||
|
|
||||||
|
function Notifications:render()
|
||||||
|
local notifs = {}
|
||||||
|
|
||||||
|
for index, notif in ipairs(self.props.notifications) do
|
||||||
|
notifs[notif] = e(Notification, {
|
||||||
|
text = notif.text,
|
||||||
|
timestamp = notif.timestamp,
|
||||||
|
timeout = notif.timeout,
|
||||||
|
layoutOrder = (notif.timestamp - baseClock),
|
||||||
|
onClose = function()
|
||||||
|
self.props.onClose(index)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return Roact.createFragment(notifs)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Notifications
|
||||||
@@ -9,6 +9,7 @@ local Roact = require(Rojo.Roact)
|
|||||||
local defaultSettings = {
|
local defaultSettings = {
|
||||||
openScriptsExternally = false,
|
openScriptsExternally = false,
|
||||||
twoWaySync = false,
|
twoWaySync = false,
|
||||||
|
showNotifications = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
local Settings = {}
|
local Settings = {}
|
||||||
@@ -118,4 +119,4 @@ end
|
|||||||
return {
|
return {
|
||||||
StudioProvider = StudioProvider,
|
StudioProvider = StudioProvider,
|
||||||
with = with,
|
with = with,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,12 +202,20 @@ function SettingsPage:render()
|
|||||||
layoutOrder = 1,
|
layoutOrder = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
ShowNotifications = e(Setting, {
|
||||||
|
id = "showNotifications",
|
||||||
|
name = "Show Notifications",
|
||||||
|
description = "Popup notifications in viewport",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
TwoWaySync = e(Setting, {
|
TwoWaySync = e(Setting, {
|
||||||
id = "twoWaySync",
|
id = "twoWaySync",
|
||||||
name = "Two-Way Sync",
|
name = "Two-Way Sync",
|
||||||
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 2,
|
layoutOrder = 3,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Layout = e("UIListLayout", {
|
Layout = e("UIListLayout", {
|
||||||
@@ -227,4 +235,4 @@ function SettingsPage:render()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return SettingsPage
|
return SettingsPage
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ local lightTheme = strict("LightTheme", {
|
|||||||
LogoColor = BRAND_COLOR,
|
LogoColor = BRAND_COLOR,
|
||||||
VersionColor = hexColor(0x727272),
|
VersionColor = hexColor(0x727272),
|
||||||
},
|
},
|
||||||
|
Notification = {
|
||||||
|
InfoColor = hexColor(0x00000),
|
||||||
|
CloseColor = BRAND_COLOR,
|
||||||
|
},
|
||||||
ErrorColor = hexColor(0x000000),
|
ErrorColor = hexColor(0x000000),
|
||||||
ScrollBarColor = hexColor(0x000000),
|
ScrollBarColor = hexColor(0x000000),
|
||||||
})
|
})
|
||||||
@@ -177,6 +181,10 @@ local darkTheme = strict("DarkTheme", {
|
|||||||
LogoColor = BRAND_COLOR,
|
LogoColor = BRAND_COLOR,
|
||||||
VersionColor = hexColor(0xD3D3D3)
|
VersionColor = hexColor(0xD3D3D3)
|
||||||
},
|
},
|
||||||
|
Notification = {
|
||||||
|
InfoColor = hexColor(0xFFFFFF),
|
||||||
|
CloseColor = hexColor(0xFFFFFF),
|
||||||
|
},
|
||||||
ErrorColor = hexColor(0xFFFFFF),
|
ErrorColor = hexColor(0xFFFFFF),
|
||||||
ScrollBarColor = hexColor(0xFFFFFF),
|
ScrollBarColor = hexColor(0xFFFFFF),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ local Theme = require(script.Theme)
|
|||||||
local PluginSettings = require(script.PluginSettings)
|
local PluginSettings = require(script.PluginSettings)
|
||||||
|
|
||||||
local Page = require(script.Page)
|
local Page = require(script.Page)
|
||||||
|
local Notifications = require(script.Notifications)
|
||||||
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
||||||
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
||||||
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
|
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
|
||||||
@@ -44,10 +45,37 @@ function App:init()
|
|||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
guiEnabled = false,
|
guiEnabled = false,
|
||||||
|
notifications = {},
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function App:addNotification(text: string, timeout: number?)
|
||||||
|
if not self.props.settings:get("showNotifications") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local notifications = table.clone(self.state.notifications)
|
||||||
|
table.insert(notifications, {
|
||||||
|
text = text,
|
||||||
|
timestamp = DateTime.now().UnixTimestampMillis,
|
||||||
|
timeout = timeout or 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
notifications = notifications,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:closeNotification(index: number)
|
||||||
|
local notifications = table.clone(self.state.notifications)
|
||||||
|
table.remove(notifications, index)
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
notifications = notifications,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
function App:getHostAndPort()
|
function App:getHostAndPort()
|
||||||
local host = self.host:getValue()
|
local host = self.host:getValue()
|
||||||
local port = self.port:getValue()
|
local port = self.port:getValue()
|
||||||
@@ -81,6 +109,7 @@ function App:startSession()
|
|||||||
appStatus = AppStatus.Connecting,
|
appStatus = AppStatus.Connecting,
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
|
self:addNotification("Connecting to session...")
|
||||||
elseif status == ServeSession.Status.Connected then
|
elseif status == ServeSession.Status.Connected then
|
||||||
local address = ("%s:%s"):format(host, port)
|
local address = ("%s:%s"):format(host, port)
|
||||||
self:setState({
|
self:setState({
|
||||||
@@ -89,8 +118,7 @@ function App:startSession()
|
|||||||
address = address,
|
address = address,
|
||||||
toolbarIcon = Assets.Images.PluginButtonConnected,
|
toolbarIcon = Assets.Images.PluginButtonConnected,
|
||||||
})
|
})
|
||||||
|
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
|
||||||
Log.info("Connected to session '{}' at {}", details, address)
|
|
||||||
elseif status == ServeSession.Status.Disconnected then
|
elseif status == ServeSession.Status.Disconnected then
|
||||||
self.serveSession = nil
|
self.serveSession = nil
|
||||||
|
|
||||||
@@ -104,13 +132,13 @@ function App:startSession()
|
|||||||
errorMessage = tostring(details),
|
errorMessage = tostring(details),
|
||||||
toolbarIcon = Assets.Images.PluginButtonWarning,
|
toolbarIcon = Assets.Images.PluginButtonWarning,
|
||||||
})
|
})
|
||||||
|
self:addNotification(tostring(details), 10)
|
||||||
else
|
else
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
|
self:addNotification("Disconnected from session.")
|
||||||
Log.info("Disconnected session")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -236,6 +264,21 @@ function App:render()
|
|||||||
end),
|
end),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
RojoNotifications = e("ScreenGui", {}, {
|
||||||
|
layout = e("UIListLayout", {
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
notifs = e(Notifications, {
|
||||||
|
notifications = self.state.notifications,
|
||||||
|
onClose = function(index)
|
||||||
|
self:closeNotification(index)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
toggleAction = e(StudioPluginAction, {
|
toggleAction = e(StudioPluginAction, {
|
||||||
name = "RojoConnection",
|
name = "RojoConnection",
|
||||||
title = "Rojo: Connect/Disconnect",
|
title = "Rojo: Connect/Disconnect",
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ local Assets = {
|
|||||||
[500] = "rbxassetid://2609138523"
|
[500] = "rbxassetid://2609138523"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Sounds = {
|
||||||
|
Notification = "rbxassetid://9716079936",
|
||||||
|
},
|
||||||
StartSession = "",
|
StartSession = "",
|
||||||
SessionActive = "",
|
SessionActive = "",
|
||||||
Configure = "",
|
Configure = "",
|
||||||
@@ -62,4 +65,4 @@ end
|
|||||||
|
|
||||||
guardForTypos("Assets", Assets)
|
guardForTypos("Assets", Assets)
|
||||||
|
|
||||||
return Assets
|
return Assets
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ local App = require(script.App)
|
|||||||
local app = Roact.createElement(App, {
|
local app = Roact.createElement(App, {
|
||||||
plugin = plugin
|
plugin = plugin
|
||||||
})
|
})
|
||||||
local tree = Roact.mount(app, nil, "Rojo UI")
|
local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI")
|
||||||
|
|
||||||
plugin.Unloading:Connect(function()
|
plugin.Unloading:Connect(function()
|
||||||
Roact.unmount(tree)
|
Roact.unmount(tree)
|
||||||
@@ -28,4 +28,4 @@ if Config.isDevBuild then
|
|||||||
local TestEZ = require(script.Parent.TestEZ)
|
local TestEZ = require(script.Parent.TestEZ)
|
||||||
|
|
||||||
require(script.runTests)(TestEZ)
|
require(script.runTests)(TestEZ)
|
||||||
end
|
end
|
||||||
|
|||||||
22
plugin/src/playSound.lua
Normal file
22
plugin/src/playSound.lua
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Roblox decided that sounds only play in Edit mode when parented to a plugin widget, for some reason
|
||||||
|
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
|
||||||
|
local widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new(
|
||||||
|
Enum.InitialDockState.Float,
|
||||||
|
false, true,
|
||||||
|
10, 10,
|
||||||
|
10, 10
|
||||||
|
))
|
||||||
|
widget.Name = "Rojo_soundPlayer"
|
||||||
|
widget.Title = "Rojo Sound Player"
|
||||||
|
|
||||||
|
return function(soundId)
|
||||||
|
local sound = Instance.new("Sound")
|
||||||
|
sound.SoundId = soundId
|
||||||
|
sound.Parent = widget
|
||||||
|
|
||||||
|
sound.Ended:Connect(function()
|
||||||
|
sound:Destroy()
|
||||||
|
end)
|
||||||
|
|
||||||
|
sound:Play()
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user