forked from rojo-rbx/rojo
Sync reminder notification & notification actions (#689)
Implements and closes #652. --------- Co-authored-by: Chris Chang <51393127+chriscerie@users.noreply.github.com> Co-authored-by: Micah <git@dekkonot.com>
This commit is contained in:
@@ -5,12 +5,14 @@
|
||||
* Fixed the diff visualizer of connected sessions. ([#674])
|
||||
* Fixed disconnected session activity. ([#675])
|
||||
* Skip confirming patches that contain only a datamodel name change. ([#688])
|
||||
* Added sync reminder notification. ([#689])
|
||||
* Added protection against syncing a model to a place. ([#691])
|
||||
|
||||
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
||||
[#674]: https://github.com/rojo-rbx/rojo/pull/674
|
||||
[#675]: https://github.com/rojo-rbx/rojo/pull/675
|
||||
[#688]: https://github.com/rojo-rbx/rojo/pull/688
|
||||
[#689]: https://github.com/rojo-rbx/rojo/pull/689
|
||||
[#691]: https://github.com/rojo-rbx/rojo/pull/691
|
||||
|
||||
## [7.3.0] - April 22, 2023
|
||||
|
||||
@@ -7,6 +7,7 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local bindingUtil = require(script.Parent.bindingUtil)
|
||||
|
||||
@@ -14,6 +15,7 @@ local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
|
||||
local baseClock = DateTime.now().UnixTimestampMillis
|
||||
|
||||
@@ -28,10 +30,8 @@ function Notification:init()
|
||||
self.lifetime = self.props.timeout
|
||||
|
||||
self.motor:onStep(function(value)
|
||||
if value <= 0 then
|
||||
if self.props.onClose then
|
||||
self.props.onClose()
|
||||
end
|
||||
if value <= 0 and self.props.onClose then
|
||||
self.props.onClose()
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -86,23 +86,54 @@ function Notification:willUnmount()
|
||||
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 textBounds = TextService:GetTextSize(
|
||||
self.props.text,
|
||||
15,
|
||||
Enum.Font.GothamMedium,
|
||||
Vector2.new(350, 700)
|
||||
)
|
||||
|
||||
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 += TextService:GetTextSize(
|
||||
action.text, 18, Enum.Font.GothamMedium,
|
||||
Vector2.new(math.huge, math.huge)
|
||||
).X + 30
|
||||
|
||||
count += 1
|
||||
end
|
||||
|
||||
buttonsX += (count - 1) * 5
|
||||
end
|
||||
|
||||
local paddingY, logoSize = 20, 32
|
||||
local actionsY = if self.props.actions then 35 else 0
|
||||
local contentX = math.max(textBounds.X, buttonsX)
|
||||
|
||||
local size = self.binding:map(function(value)
|
||||
return UDim2.fromOffset(
|
||||
(35+40+textBounds.X)*value,
|
||||
math.max(14+20+textBounds.Y, 32+20)
|
||||
(35 + 40 + contentX) * value,
|
||||
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
||||
)
|
||||
end)
|
||||
|
||||
@@ -122,22 +153,22 @@ function Notification:render()
|
||||
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),
|
||||
Contents = e("Frame", {
|
||||
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
|
||||
Position = UDim2.new(0, 0, 0, paddingY / 2),
|
||||
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),
|
||||
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,
|
||||
Font = Enum.Font.GothamSemibold,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 15,
|
||||
TextColor3 = theme.Notification.InfoColor,
|
||||
TextTransparency = transparency,
|
||||
@@ -150,20 +181,21 @@ function Notification:render()
|
||||
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,
|
||||
Actions = if self.props.actions then e("Frame", {
|
||||
Size = UDim2.new(1, -40, 0, 35),
|
||||
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", {
|
||||
@@ -180,15 +212,16 @@ local Notifications = Roact.Component:extend("Notifications")
|
||||
function Notifications:render()
|
||||
local notifs = {}
|
||||
|
||||
for index, notif in ipairs(self.props.notifications) do
|
||||
notifs[notif] = e(Notification, {
|
||||
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(index)
|
||||
self.props.onClose(id)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
@@ -59,6 +59,7 @@ function Setting:render()
|
||||
LayoutOrder = self.props.layoutOrder,
|
||||
ZIndex = -self.props.layoutOrder,
|
||||
BackgroundTransparency = 1,
|
||||
Visible = self.props.visible,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(object)
|
||||
self.setContainerSize(object.AbsoluteSize)
|
||||
|
||||
@@ -87,19 +87,20 @@ function SettingsPage:render()
|
||||
layoutOrder = 0,
|
||||
}),
|
||||
|
||||
OpenScriptsExternally = e(Setting, {
|
||||
id = "openScriptsExternally",
|
||||
name = "Open Scripts Externally",
|
||||
description = "Attempt to open scripts in an external editor",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
ShowNotifications = e(Setting, {
|
||||
id = "showNotifications",
|
||||
name = "Show Notifications",
|
||||
description = "Popup notifications in viewport",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
SyncReminder = e(Setting, {
|
||||
id = "syncReminder",
|
||||
name = "Sync Reminder",
|
||||
description = "Notify to sync when opening a place that has previously been synced",
|
||||
transparency = self.props.transparency,
|
||||
visible = Settings:getBinding("showNotifications"),
|
||||
layoutOrder = 2,
|
||||
}),
|
||||
|
||||
@@ -111,12 +112,20 @@ function SettingsPage:render()
|
||||
layoutOrder = 3,
|
||||
}),
|
||||
|
||||
OpenScriptsExternally = e(Setting, {
|
||||
id = "openScriptsExternally",
|
||||
name = "Open Scripts Externally",
|
||||
description = "EXPERIMENTAL! Attempt to open scripts in an external editor",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 4,
|
||||
}),
|
||||
|
||||
TwoWaySync = e(Setting, {
|
||||
id = "twoWaySync",
|
||||
name = "Two-Way Sync",
|
||||
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 4,
|
||||
layoutOrder = 5,
|
||||
}),
|
||||
|
||||
LogLevel = e(Setting, {
|
||||
@@ -124,7 +133,7 @@ function SettingsPage:render()
|
||||
name = "Log Level",
|
||||
description = "Plugin output verbosity level",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 5,
|
||||
layoutOrder = 100,
|
||||
|
||||
options = invertedLevels,
|
||||
showReset = Settings:getBinding("logLevel"):map(function(value)
|
||||
@@ -140,7 +149,7 @@ function SettingsPage:render()
|
||||
name = "Typechecking",
|
||||
description = "Toggle typechecking on the API surface",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 6,
|
||||
layoutOrder = 101,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
local Players = game:GetService("Players")
|
||||
local ServerStorage = game:GetService("ServerStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
@@ -19,6 +20,7 @@ local ApiContext = require(Plugin.ApiContext)
|
||||
local PatchSet = require(Plugin.PatchSet)
|
||||
local preloadAssets = require(Plugin.preloadAssets)
|
||||
local soundPlayer = require(Plugin.soundPlayer)
|
||||
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
|
||||
local Theme = require(script.Theme)
|
||||
|
||||
local Page = require(script.Page)
|
||||
@@ -53,6 +55,7 @@ function App:init()
|
||||
|
||||
self.confirmationBindable = Instance.new("BindableEvent")
|
||||
self.confirmationEvent = self.confirmationBindable.Event
|
||||
self.notifId = 0
|
||||
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
@@ -65,28 +68,63 @@ function App:init()
|
||||
notifications = {},
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
|
||||
if
|
||||
RunService:IsEdit()
|
||||
and self.serveSession == nil
|
||||
and Settings:get("syncReminder")
|
||||
and self:getLastSyncTimestamp()
|
||||
then
|
||||
self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
|
||||
Connect = {
|
||||
text = "Connect",
|
||||
style = "Solid",
|
||||
layoutOrder = 1,
|
||||
onClick = function(notification)
|
||||
notification:dismiss()
|
||||
self:startSession()
|
||||
end
|
||||
},
|
||||
Dismiss = {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
onClick = function(notification)
|
||||
notification:dismiss()
|
||||
end,
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function App:addNotification(text: string, timeout: number?)
|
||||
function App:addNotification(text: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?)
|
||||
if not Settings:get("showNotifications") then
|
||||
return
|
||||
end
|
||||
|
||||
self.notifId += 1
|
||||
local id = self.notifId
|
||||
|
||||
local notifications = table.clone(self.state.notifications)
|
||||
table.insert(notifications, {
|
||||
notifications[id] = {
|
||||
text = text,
|
||||
timestamp = DateTime.now().UnixTimestampMillis,
|
||||
timeout = timeout or 3,
|
||||
})
|
||||
actions = actions,
|
||||
}
|
||||
|
||||
self:setState({
|
||||
notifications = notifications,
|
||||
})
|
||||
|
||||
return function()
|
||||
self:closeNotification(id)
|
||||
end
|
||||
end
|
||||
|
||||
function App:closeNotification(index: number)
|
||||
function App:closeNotification(id: number)
|
||||
local notifications = table.clone(self.state.notifications)
|
||||
table.remove(notifications, index)
|
||||
notifications[id] = nil
|
||||
|
||||
self:setState({
|
||||
notifications = notifications,
|
||||
@@ -97,12 +135,28 @@ function App:getPriorEndpoint()
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then return end
|
||||
|
||||
local place = priorEndpoints[tostring(game.PlaceId)]
|
||||
local id = tostring(game.PlaceId)
|
||||
if ignorePlaceIds[id] then return end
|
||||
|
||||
local place = priorEndpoints[id]
|
||||
if not place then return end
|
||||
|
||||
return place.host, place.port
|
||||
end
|
||||
|
||||
function App:getLastSyncTimestamp()
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then return end
|
||||
|
||||
local id = tostring(game.PlaceId)
|
||||
if ignorePlaceIds[id] then return end
|
||||
|
||||
local place = priorEndpoints[id]
|
||||
if not place then return end
|
||||
|
||||
return place.timestamp
|
||||
end
|
||||
|
||||
function App:setPriorEndpoint(host: string, port: string)
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then
|
||||
@@ -117,17 +171,16 @@ function App:setPriorEndpoint(host: string, port: string)
|
||||
end
|
||||
end
|
||||
|
||||
if host == Config.defaultHost and port == Config.defaultPort then
|
||||
-- Don't save default
|
||||
priorEndpoints[tostring(game.PlaceId)] = nil
|
||||
else
|
||||
priorEndpoints[tostring(game.PlaceId)] = {
|
||||
host = host ~= Config.defaultHost and host or nil,
|
||||
port = port ~= Config.defaultPort and port or nil,
|
||||
timestamp = os.time(),
|
||||
}
|
||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||
end
|
||||
local id = tostring(game.PlaceId)
|
||||
if ignorePlaceIds[id] then return end
|
||||
|
||||
priorEndpoints[id] = {
|
||||
host = if host ~= Config.defaultHost then host else nil,
|
||||
port = if port ~= Config.defaultPort then port else nil,
|
||||
timestamp = os.time(),
|
||||
}
|
||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||
|
||||
|
||||
Settings:set("priorEndpoints", priorEndpoints)
|
||||
end
|
||||
@@ -470,7 +523,11 @@ function App:render()
|
||||
}),
|
||||
}),
|
||||
|
||||
RojoNotifications = e("ScreenGui", {}, {
|
||||
RojoNotifications = e("ScreenGui", {
|
||||
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
ResetOnSpawn = false,
|
||||
DisplayOrder = 100,
|
||||
}, {
|
||||
layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
@@ -486,8 +543,8 @@ function App:render()
|
||||
notifs = e(Notifications, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
notifications = self.state.notifications,
|
||||
onClose = function(index)
|
||||
self:closeNotification(index)
|
||||
onClose = function(id)
|
||||
self:closeNotification(id)
|
||||
end,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -13,6 +13,7 @@ local defaultSettings = {
|
||||
openScriptsExternally = false,
|
||||
twoWaySync = false,
|
||||
showNotifications = true,
|
||||
syncReminder = true,
|
||||
playSounds = true,
|
||||
typecheckingEnabled = false,
|
||||
logLevel = "Info",
|
||||
|
||||
27
plugin/src/ignorePlaceIds.lua
Normal file
27
plugin/src/ignorePlaceIds.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
--[[
|
||||
These are place ids that will not have metadata saved for them,
|
||||
such as last sync address or time. This is because they are not unique
|
||||
so storing metadata for them does not make sense as these ids are reused.
|
||||
--]]
|
||||
|
||||
return {
|
||||
["0"] = true, -- Local file
|
||||
["95206881"] = true, -- Baseplate
|
||||
["6560363541"] = true, -- Classic Baseplate
|
||||
["95206192"] = true, -- Flat Terrain
|
||||
["13165709401"] = true, -- Modern City
|
||||
["520390648"] = true, -- Village
|
||||
["203810088"] = true, -- Castle
|
||||
["366130569"] = true, -- Suburban
|
||||
["215383192"] = true, -- Racing
|
||||
["264719325"] = true, -- Pirate Island
|
||||
["203812057"] = true, -- Obby
|
||||
["379736082"] = true, -- Starting Place
|
||||
["301530843"] = true, -- Line Runner
|
||||
["92721754"] = true, -- Capture The Flag
|
||||
["301529772"] = true, -- Team/FFA Arena
|
||||
["203885589"] = true, -- Combat
|
||||
["10275826693"] = true, -- Concert
|
||||
["5353920686"] = true, -- Move It Simulator
|
||||
["6936227200"] = true, -- Mansion Of Wonder
|
||||
}
|
||||
Reference in New Issue
Block a user