Files
sky_phone-sky-systems/sky_phone/source/client/main.lua
T
Leon.Schmidt 332060b8e3 ENH - modernize FlipTok and Messages
Replace the mixed legacy Konsta presentation and fixed light surfaces with the shared Sky UI system. Add FlipTok comment replies and reactions, editable profile media, follower lists, external sound metadata, and the required schema migration.
2026-08-14 01:28:48 +02:00

1004 lines
31 KiB
Lua

local is_open = false
local open_requested = false
local notification_focus = false
local call_focus = false
local payphone_focus = false
local camera_active = false
local camera_nui_focused = true
local device_payload = nil
local sim_picker_open = false
local sim_picker_payload = nil
local active_call_payload = nil
local call_channel = 0
local nui_generation = 0
local activity_suspended = false
Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true })
local server_callbacks = {
"security:unlock",
"security:set-passcode",
"security:change-passcode",
"security:disable-passcode",
"device:save",
"device:notification-open",
"notifications:save",
"custom-app:storage:get",
"custom-app:storage:set",
"device:factory-reset",
"account:login",
"account:register",
"account:logout",
"account:devices",
"account:remove-device",
"notes:list",
"notes:create",
"notes:update",
"notes:delete",
"mail:register",
"mail:login",
"mail:logout",
"mail:counts",
"mail:list",
"mail:get",
"mail:get-draft",
"mail:save-draft",
"mail:delete-draft",
"mail:delete-many",
"mail:send",
"mail:set-read",
"mail:trash",
"mail:restore",
"mail:delete-forever",
"mail:empty-trash",
"marketplace:list",
"marketplace:profile",
"marketplace:profile-save",
"marketplace:get",
"marketplace:list-own",
"marketplace:create",
"marketplace:update",
"marketplace:set-status",
"marketplace:favorite",
"marketplace:counts",
"marketplace:list-inquiries",
"marketplace:get-inquiry",
"marketplace:send-message",
"marketplace:make-offer",
"marketplace:respond-offer",
"marketplace:report",
"marketplace:block",
"pages:list",
"pages:get",
"pages:profile",
"pages:profile-save",
"pages:list-own",
"pages:create",
"pages:share-citymarkt",
"pages:react",
"pages:delete",
"weazel-news:context",
"weazel-news:list",
"weazel-news:get",
"weazel-news:manage-list",
"weazel-news:create",
"weazel-news:update",
"weazel-news:delete",
"fliptok:register",
"fliptok:login",
"fliptok:logout",
"fliptok:bootstrap",
"fliptok:feed",
"fliptok:video",
"fliptok:discover",
"fliptok:music-metadata",
"fliptok:publish",
"fliptok:react",
"fliptok:follow",
"fliptok:comments",
"fliptok:comment",
"fliptok:comment-react",
"fliptok:view",
"fliptok:share",
"fliptok:profile",
"fliptok:connections",
"fliptok:update-profile",
"fliptok:activities",
"fliptok:mark-activities",
"fliptok:report",
"fliptok:admin-reports",
"fliptok:admin-resolve-report",
"fliptok:block",
"fliptok:delete",
"picstagram:register",
"picstagram:login",
"picstagram:logout",
"picstagram:bootstrap",
"picstagram:feed",
"picstagram:post",
"picstagram:explore",
"picstagram:search",
"picstagram:saved",
"picstagram:profile",
"picstagram:update-profile",
"picstagram:publish-post",
"picstagram:update-post",
"picstagram:set-post-status",
"picstagram:react",
"picstagram:follow",
"picstagram:respond-follow",
"picstagram:comments",
"picstagram:comment",
"picstagram:remove-comment",
"picstagram:publish-story",
"picstagram:stories",
"picstagram:view-story",
"picstagram:story-viewers",
"picstagram:remove-story",
"picstagram:activities",
"picstagram:mark-activities",
"picstagram:block",
"picstagram:report",
"picstagram:admin-reports",
"picstagram:admin-resolve-report",
"feather:bootstrap",
"feather:create-profile",
"feather:update-profile",
"feather:feed",
"feather:explore",
"feather:network",
"feather:create-post",
"feather:react",
"feather:follow",
"feather:connections",
"feather:remove-connection",
"feather:profile",
"feather:bookmarks",
"feather:thread",
"feather:activities",
"feather:mark-activities",
"feather:delete",
"feather:block",
"feather:report",
"calendar:list",
"calendar:create",
"calendar:update",
"calendar:delete",
"music:bootstrap",
"music:add-youtube",
"music:remove-youtube",
"music:create-playlist",
"music:rename-playlist",
"music:delete-playlist",
"music:add-to-playlist",
"music:remove-from-playlist",
"map:markers",
"map:create-marker",
"map:delete-marker",
"crewlink:bootstrap",
"crewlink:create-profile",
"crewlink:update-profile",
"crewlink:create-group",
"crewlink:update-group",
"crewlink:delete-group",
"crewlink:set-active",
"crewlink:join-code",
"crewlink:rotate-code",
"crewlink:nearby",
"crewlink:invite-nearby",
"crewlink:respond-invite",
"crewlink:update-member",
"crewlink:transfer-owner",
"crewlink:remove-member",
"crewlink:leave",
"crewlink:create-ping",
"crewlink:remove-ping",
"companies:list",
"companies:get",
"companies:my-requests",
"companies:get-request",
"companies:work-context",
"companies:work-queue",
"companies:list-members",
"companies:create-request",
"companies:cancel-request",
"companies:send-message",
"companies:claim-request",
"companies:assign-request",
"companies:update-request-status",
"companies:update-availability",
"companies:update-profile",
"companies:update-hours",
"companies:update-services",
"companies:publish-announcement",
"companies:set-call-availability",
"companies:call-customer",
"sim:insert",
"sim:eject",
"contacts:list",
"contacts:save",
"contacts:delete",
"contacts:favorite",
"calls:recents",
"calls:dial",
"calls:answer",
"calls:decline",
"calls:hangup",
"calls:block",
"banking:overview",
"banking:transfer",
"billing:overview",
"billing:list",
"billing:detail",
"billing:markRead",
"billing:pay",
"billing:dispute",
"messages:conversations",
"messages:thread",
"messages:send",
"messages:media",
"messages:delete",
"messages:gifs",
"memos:list",
"memos:update",
"memos:delete",
"easyshare:bootstrap",
"easyshare:own-contact",
"easyshare:set-visibility",
"easyshare:request",
"easyshare:respond",
"easyshare:cancel",
"darkchat:bootstrap",
"darkchat:create-profile",
"darkchat:delete-profile",
"darkchat:update-profile",
"darkchat:start",
"darkchat:thread",
"darkchat:send",
"darkchat:media",
"darkchat:react",
"darkchat:message-action",
"darkchat:update-conversation",
"darkchat:add-contact",
"darkchat:remove-contact",
"darkchat:block",
"darkchat:report",
"darkchat:clear",
"flare:bootstrap",
"flare:save-profile",
"flare:set-discovery",
"flare:swipe",
"flare:rewind",
"flare:unmatch",
"flare:thread",
"flare:send",
"gallery:list",
"media:config",
"media:import:sources",
"media:import:list",
"media:import:commit",
"media:import:url",
}
local function get_locale()
return Locales[Config.Bridge.Locale] or Locales["en"]
end
local function update_nui_focus()
local focus = SkyPhoneFocus.Resolve({
activity_suspended = activity_suspended,
call_focus = call_focus,
camera_active = camera_active,
camera_nui_focused = camera_nui_focused,
is_open = is_open,
notification_focus = notification_focus,
payphone_focus = payphone_focus,
sim_picker_open = sim_picker_open,
})
SetNuiFocus(focus.focused, focus.focused)
SetNuiFocusKeepInput(focus.keep_input)
TriggerEvent("sky_phone:client:cameraFocusApplied", {
active = camera_active,
focused = focus.focused,
gameInput = focus.keep_input,
})
end
AddEventHandler("sky_phone:client:setSuspended", function(suspended)
activity_suspended = suspended == true
update_nui_focus()
end)
AddEventHandler("sky_phone:client:setPayphoneFocus", function(focused)
payphone_focus = focused == true
update_nui_focus()
end)
AddEventHandler("sky_phone:client:setCameraFocus", function(data)
if type(data) ~= "table" or type(data.active) ~= "boolean" or type(data.nuiFocused) ~= "boolean" then
Bridge.Debug("error", "[sky_phone] Rejected invalid camera focus claim.")
return
end
camera_active = data.active
camera_nui_focused = data.nuiFocused
update_nui_focus()
end)
local function send_open_message()
if not device_payload then
return
end
local payload = device_payload
payload.lang = Config.Bridge.Locale
payload.locales = get_locale().Nui
SkyPhoneApps.SendCatalog()
SendNUIMessage({
type = "app:open",
data = payload,
})
end
local function open_phone()
if is_open or not device_payload then
return
end
send_open_message()
end
local function close_phone(close_device_session)
local was_requested = open_requested
local was_open = is_open
open_requested = false
call_focus = false
activity_suspended = false
TriggerEvent("sky_phone:animation:phone", false)
is_open = false
if was_open then
SkyPhoneApps.SetPhoneOpen(false)
TriggerEvent("sky_phone:nuiClosed")
end
update_nui_focus()
if was_requested or was_open then
SendNUIMessage({ type = "app:close" })
if close_device_session ~= false then
Bridge.Callbacks.Trigger("sky_phone:device:close", {})
end
end
end
AddEventHandler("sky_phone:client:forceClose", function()
close_phone()
end)
local function leave_call_voice()
if call_channel == 0 then
return
end
if Config.Calls.VoiceProvider == "pma" and GetResourceState("pma-voice") == "started" then
exports["pma-voice"]:setCallChannel(0)
end
call_channel = 0
end
local function join_call_voice(channel)
if Config.Calls.VoiceProvider ~= "pma" then
Bridge.Debug("error", "[sky_phone] Unsupported voice provider '%s'.", tostring(Config.Calls.VoiceProvider))
return false
end
if GetResourceState("pma-voice") ~= "started" then
Bridge.Debug("error", "[sky_phone] Configured pma-voice provider is not started.")
return false
end
call_channel = tonumber(channel) or 0
exports["pma-voice"]:setCallChannel(call_channel)
return true
end
if Config.Phone.DevelopmentCommand then
RegisterCommand(Config.Command, function()
if is_open then
close_phone()
return
end
Bridge.Callbacks.Trigger("sky_phone:device:development-open", {})
end, false)
end
RegisterNetEvent("sky_phone:testdata:feedback", function(success, detail)
local locale = get_locale().TestData
local message = success and locale.Success or locale.Failed
if success and type(detail) == "string" and detail ~= "" then
message = message:gsub("{email}", detail)
end
Bridge.Framework.Notify("iFruit", message, success and "success" or "error", 7000)
end)
RegisterNUICallback("ui:ready", function(data, cb)
if type(data) ~= "table" or data.protocolVersion ~= 1 then
cb({ success = false, error = "unsupported_protocol" })
return
end
nui_generation = nui_generation + 1
-- Browser state is recreated on a CEF reload. A notification focus claim
-- cannot survive unless its notification is replayed as part of this handshake.
notification_focus = false
Bridge.Debug("debug", "[sky_phone] NUI reported ready.", { always = true })
SkyPhoneApps.SendCatalog()
if open_requested and device_payload then
send_open_message()
end
if active_call_payload then
SendNUIMessage({ type = "call:state", data = active_call_payload })
end
if sim_picker_open and sim_picker_payload then
SendNUIMessage({ type = "sim:picker", data = sim_picker_payload })
else
SendNUIMessage({ type = "sim:picker-close" })
end
update_nui_focus()
TriggerEvent("sky_phone:client:nuiReady", {
generation = nui_generation,
protocolVersion = 1,
})
cb({
success = true,
data = {
generation = nui_generation,
protocolVersion = 1,
},
})
end)
RegisterNUICallback("ui:opened", function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
return
end
if not open_requested or not device_payload then
Bridge.Debug(
"warn",
"[sky_phone] Ignored a NUI open confirmation without a pending device open.",
{ always = true }
)
SendNUIMessage({ type = "app:close" })
update_nui_focus()
cb({ success = false, error = "open_not_requested" })
return
end
is_open = true
SkyPhoneApps.SetPhoneOpen(true)
notification_focus = false
call_focus = false
update_nui_focus()
TriggerEvent("sky_phone:animation:phone", true)
cb({ success = true })
end)
RegisterNUICallback("close", function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
return
end
close_phone()
cb({ success = true })
end)
RegisterNUICallback("notification:focus", function(data, cb)
if type(data) ~= "table" or type(data.active) ~= "boolean" then
cb({ success = false, error = "invalid_request" })
return
end
notification_focus = data.active == true and not is_open
update_nui_focus()
cb({ success = true })
end)
RegisterNUICallback("sim:picker-close", function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
return
end
sim_picker_open = false
sim_picker_payload = nil
update_nui_focus()
SendNUIMessage({ type = "sim:picker-close" })
local result = Bridge.Callbacks.Trigger("sky_phone:sim:picker-close", {})
cb(type(result) == "table" and result or { success = false, error = "request_failed" })
end)
RegisterNUICallback("map:getPlayerCoords", function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
return
end
local coords = GetEntityCoords(PlayerPedId())
cb({
success = true,
data = {
coords = {
x = coords.x,
y = coords.y,
z = coords.z
}
}
})
end)
RegisterNUICallback("map:setWaypoint", function(data, cb)
local coords = type(data) == "table" and data.coords or nil
local x = type(coords) == "table" and tonumber(coords.x) or nil
local y = type(coords) == "table" and tonumber(coords.y) or nil
if not x or not y or x ~= x or y ~= y or math.abs(x) > 10000.0 or math.abs(y) > 10000.0 then
cb({ success = false, error = "invalid_marker" })
return
end
SetNewWaypoint(x, y)
cb({ success = true })
end)
local weather_types = {
[joaat("EXTRASUNNY")] = "sunny",
[joaat("CLEAR")] = "clear",
[joaat("CLOUDS")] = "partly_cloudy",
[joaat("OVERCAST")] = "cloudy",
[joaat("RAIN")] = "rain",
[joaat("CLEARING")] = "rain",
[joaat("THUNDER")] = "thunder",
[joaat("SMOG")] = "fog",
[joaat("FOGGY")] = "fog",
[joaat("NEUTRAL")] = "cloudy",
[joaat("SNOW")] = "snow",
[joaat("BLIZZARD")] = "snow",
[joaat("SNOWLIGHT")] = "snow",
[joaat("XMAS")] = "snow",
[joaat("HALLOWEEN")] = "cloudy",
}
local function weather_region(coords)
if coords.x > 2500.0 and coords.y < -3000.0 then
return "cayo_perico"
end
if coords.y > 900.0 then
return "blaine_county"
end
return "los_santos"
end
RegisterNUICallback("weather:get", function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
return
end
local coords = GetEntityCoords(PlayerPedId())
local weather_hash = GetPrevWeatherTypeHashName()
local next_weather_hash = GetNextWeatherTypeHashName()
cb({
success = true,
data = {
condition = weather_types[weather_hash] or "clear",
nextCondition = weather_types[next_weather_hash] or weather_types[weather_hash] or "clear",
region = weather_region(coords),
clock = {
year = GetClockYear(),
month = GetClockMonth() + 1,
day = GetClockDayOfMonth(),
hour = GetClockHours(),
minute = GetClockMinutes(),
},
windSpeed = math.max(0.0, GetWindSpeed()),
rainLevel = math.max(0.0, math.min(1.0, GetRainLevel())),
},
})
end)
local function garage_vehicle_kind(model_hash, fallback)
if IsThisModelABoat(model_hash) then
return "boat"
end
if IsThisModelAPlane(model_hash) then
return "plane"
end
if IsThisModelAHeli(model_hash) then
return "helicopter"
end
if IsThisModelABike(model_hash) or IsThisModelABicycle(model_hash) then
return "bike"
end
return fallback
end
RegisterNUICallback("garage:vehicles", function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
return
end
local result = Bridge.Callbacks.Trigger("sky_phone:garage:vehicles", data)
if type(result) ~= "table" or not result.success or type(result.data) ~= "table" then
cb(type(result) == "table" and result or { success = false, error = "request_failed" })
return
end
for _, vehicle in ipairs(result.data.vehicles or {}) do
local model_hash = tonumber(vehicle.model)
if not model_hash and type(vehicle.model) == "string" and vehicle.model ~= "" then
model_hash = joaat(vehicle.model)
end
if model_hash then
local display_name = GetDisplayNameFromVehicleModel(model_hash)
local label = display_name and GetLabelText(display_name) or nil
if label and label ~= "NULL" and label ~= "CARNOTFOUND" then
vehicle.name = label
elseif type(vehicle.model) == "string" and vehicle.model ~= "" then
vehicle.name = vehicle.model
end
vehicle.kind = garage_vehicle_kind(model_hash, vehicle.kind)
end
end
cb(result)
end)
for _, callback_name in ipairs(server_callbacks) do
RegisterNUICallback(callback_name, function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
return
end
local result = Bridge.Callbacks.Trigger("sky_phone:" .. callback_name, data)
if callback_name:match("^media:import:") and (not result or not result.success) then
Bridge.Debug(
"warn",
"[sky_phone] NUI callback '%s' failed: %s.",
callback_name,
tostring(result and result.error or "no server response"),
{ always = true }
)
end
if type(result) == "table" then
cb(result)
return
end
cb({ success = false, error = "request_failed" })
end)
end
RegisterNetEvent("sky_phone:device:open", function(data)
if type(data) ~= "table" or type(data.device) ~= "table" or type(data.device.imei) ~= "string" then
Bridge.Debug("error", "[sky_phone] Rejected invalid device open data.")
return
end
Bridge.Debug(
"debug",
"[sky_phone] Client received device open for IMEI %s account_linked=%s.",
tostring(data.device.imei),
tostring(data.account ~= nil),
{ always = true }
)
device_payload = data
open_requested = true
if is_open then
SkyPhoneApps.SendCatalog()
SendNUIMessage({ type = "device:updated", data = data })
return
end
open_phone()
end)
RegisterNetEvent("sky_phone:device:updated", function(data)
if type(data) ~= "table" or type(data.device) ~= "table" or type(data.device.imei) ~= "string" then
Bridge.Debug("error", "[sky_phone] Rejected invalid device update data.")
return
end
device_payload = data
SendNUIMessage({ type = "device:updated", data = data })
end)
RegisterNetEvent("sky_phone:device:invalidated", function()
TriggerEvent("sky_phone:animation:reset")
close_phone(false)
device_payload = nil
end)
RegisterNetEvent("sky_phone:device:error", function(error_code)
Bridge.Debug(
"debug",
"[sky_phone] Client received device error: %s.",
tostring(error_code),
{ always = true }
)
local message = get_locale().DeviceErrors[error_code] or get_locale().DeviceErrors.default
Bridge.Framework.Notify("iFruit", message, "error", 5000)
end)
RegisterNetEvent("sky_phone:mail:changed", function(data)
SendNUIMessage({ type = "mail:changed", data = data })
end)
RegisterNetEvent("sky_phone:easyshare:changed", function(data)
SendNUIMessage({ type = "easyshare:changed", data = data })
end)
RegisterNetEvent("sky_phone:gallery:changed", function()
SendNUIMessage({ type = "gallery:changed" })
end)
RegisterNetEvent("sky_phone:mail:new", function(data)
local mail_locale = get_locale().Nui.Apps.mail
data.title = mail_locale.name
data.text = mail_locale.newMessage:gsub("{sender}", tostring(data.sender))
SendNUIMessage({ type = "mail:new", data = data })
end)
RegisterNetEvent("sky_phone:marketplace:changed", function(data)
SendNUIMessage({ type = "marketplace:changed", data = data })
end)
RegisterNetEvent("sky_phone:companies:changed", function(data)
SendNUIMessage({ type = "companies:changed", data = data })
end)
RegisterNetEvent("sky_phone:companies:notification", function(data)
local companies_locale = get_locale().Nui.Apps.companies
data.title = companies_locale.name
data.text = companies_locale.notifications[data.kind]
or companies_locale.notifications.requestUpdated
SendNUIMessage({ type = "companies:notification", data = data })
end)
RegisterNetEvent("sky_phone:fliptok:command-feedback", function(data)
Bridge.Framework.Notify("FlipTok", data.message, data.notificationType, 5000)
end)
RegisterNetEvent("sky_phone:fliptok:verification-changed", function(data)
SendNUIMessage({ type = "fliptok:verification-changed", data = data })
end)
RegisterNetEvent("sky_phone:fliptok:new", function(data)
local fliptok_locale = get_locale().Nui.Apps.fliptok
local notification_text = fliptok_locale.notifications[data.kind] or fliptok_locale.notifications.default
data.title = fliptok_locale.name
data.text = notification_text:gsub("{actor}", tostring(data.actor or ""))
SendNUIMessage({ type = "fliptok:new", data = data })
end)
RegisterNetEvent("sky_phone:picstagram:command-feedback", function(data)
Bridge.Framework.Notify("Picstagram", data.message, data.notificationType, 5000)
end)
RegisterNetEvent("sky_phone:picstagram:verification-changed", function(data)
SendNUIMessage({ type = "picstagram:verification-changed", data = data })
end)
RegisterNetEvent("sky_phone:picstagram:new", function(data)
local picstagram_locale = get_locale().Nui.Apps.picstagram
local notification_text = picstagram_locale.notifications[data.kind] or picstagram_locale.notifications.default
data.title = picstagram_locale.name
data.text = notification_text:gsub("{actor}", tostring(data.actor or ""))
SendNUIMessage({ type = "picstagram:new", data = data })
end)
RegisterNetEvent("sky_phone:feather:new", function(data)
local feather_locale = get_locale().Nui.Apps.feather
local notification_text = feather_locale.notifications[data.kind] or feather_locale.notifications.default
data.title = feather_locale.name
data.text = notification_text:gsub("{actor}", tostring(data.actor or ""))
SendNUIMessage({ type = "feather:new", data = data })
end)
RegisterNetEvent("sky_phone:marketplace:new-message", function(data)
local marketplace_locale = get_locale().Nui.Apps.citymarkt
data.title = marketplace_locale.name
if data.kind == "offer" then
data.text = marketplace_locale.newOffer
:gsub("{sender}", tostring(data.sender))
:gsub("{price}", tostring(data.amount))
elseif data.kind == "offer-response" and data.action == "accepted" then
data.text = marketplace_locale.offerAcceptedNotification
:gsub("{sender}", tostring(data.sender))
:gsub("{price}", tostring(data.amount))
elseif data.kind == "offer-response" and data.action == "rejected" then
data.text = marketplace_locale.offerRejectedNotification
:gsub("{sender}", tostring(data.sender))
:gsub("{price}", tostring(data.amount))
else
data.text = marketplace_locale.newMessage:gsub("{sender}", tostring(data.sender))
end
SendNUIMessage({ type = "marketplace:new-message", data = data })
end)
RegisterNetEvent("sky_phone:calendar:reminder", function(data)
local calendar_locale = get_locale().Nui.Apps.calendar
data.title = calendar_locale.name
data.text = calendar_locale.reminder:gsub("{title}", tostring(data.eventTitle))
SendNUIMessage({ type = "calendar:reminder", data = data })
end)
RegisterNetEvent("sky_phone:flare:match", function(data)
local flare_locale = get_locale().Nui.Apps.flare
data.title = flare_locale.name
data.text = flare_locale.newMatchNotification:gsub("{sender}", tostring(data.sender))
SendNUIMessage({ type = "flare:new-match", data = data })
end)
RegisterNetEvent("sky_phone:flare:message", function(data)
local flare_locale = get_locale().Nui.Apps.flare
data.title = flare_locale.name
data.text = flare_locale.newMessageNotification:gsub("{sender}", tostring(data.sender))
SendNUIMessage({ type = "flare:new-message", data = data })
end)
RegisterNetEvent("sky_phone:sim:picker", function(data)
if type(data) ~= "table" or type(data.number) ~= "string" or type(data.choices) ~= "table" then
Bridge.Debug("error", "[sky_phone] Rejected invalid SIM picker data.")
return
end
sim_picker_open = true
sim_picker_payload = data
update_nui_focus()
SendNUIMessage({ type = "sim:picker", data = data })
end)
RegisterNetEvent("sky_phone:sim:picker-close", function()
sim_picker_open = false
sim_picker_payload = nil
update_nui_focus()
SendNUIMessage({ type = "sim:picker-close" })
end)
RegisterNetEvent("sky_phone:contacts:changed", function()
SendNUIMessage({ type = "contacts:changed" })
end)
RegisterNetEvent("sky_phone:calls:changed", function()
SendNUIMessage({ type = "calls:changed" })
end)
RegisterNetEvent("sky_phone:banking:changed", function()
SendNUIMessage({ type = "banking:changed" })
end)
RegisterNetEvent("sky_phone:billing:changed", function()
SendNUIMessage({ type = "billing:changed" })
end)
RegisterNetEvent("sky_phone:billing:new", function(data)
local billing_locale = get_locale().Nui.Apps.billing
data.title = billing_locale.name
data.text = billing_locale.notifications.newInvoice
:gsub("{issuer}", tostring(data.issuer))
:gsub("{amount}", ("%s %s"):format(tostring(data.amount), Config.Billing.Currency))
SendNUIMessage({ type = "billing:new", data = data })
end)
RegisterNetEvent("sky_phone:crewlink:changed", function(data)
SendNUIMessage({ type = "crewlink:changed", data = data })
end)
RegisterNetEvent("sky_phone:crewlink:notification", function(data)
local crewlink_locale = get_locale().Nui.Apps.crewlink
local notification_text = crewlink_locale.notifications[data.kind]
or crewlink_locale.notifications.default
data.title = crewlink_locale.name
data.text = notification_text
:gsub("{actor}", tostring(data.actor or ""))
:gsub("{group}", tostring(data.groupName or ""))
:gsub("{ping}", tostring(data.pingLabel or ""))
SendNUIMessage({ type = "crewlink:notification", data = data })
end)
RegisterNetEvent("sky_phone:messages:changed", function(data)
SendNUIMessage({ type = "messages:changed", data = data })
end)
RegisterNetEvent("sky_phone:messages:new", function(data)
local messages_locale = get_locale().Nui.Apps.messages
data.title = messages_locale.name
data.text = messages_locale.newMessage:gsub("{sender}", tostring(data.sender))
SendNUIMessage({ type = "messages:new", data = data })
end)
RegisterNetEvent("sky_phone:darkchat:changed", function(data)
SendNUIMessage({ type = "darkchat:changed", data = data })
end)
RegisterNetEvent("sky_phone:darkchat:new", function(data)
local darkchat_locale = get_locale().Nui.Apps.darkchat
data.title = darkchat_locale.name
if data.notificationMode == "private" then
data.sender = nil
data.text = darkchat_locale.privateNotification
elseif data.notificationMode == "hidden" then
data.sender = nil
data.text = ""
else
data.text = darkchat_locale.newMessage:gsub("{sender}", tostring(data.sender))
end
SendNUIMessage({ type = "darkchat:new", data = data })
end)
RegisterNetEvent("sky_phone:call:incoming", function(data)
if type(data) ~= "table" or type(data.id) ~= "string" or data.state ~= "ringing" then
Bridge.Debug("error", "[sky_phone] Rejected invalid incoming call data.")
return
end
active_call_payload = data
call_focus = true
update_nui_focus()
TriggerEvent("sky_phone:animation:call", data)
SendNUIMessage({ type = "call:incoming", data = data })
end)
RegisterNetEvent("sky_phone:call:state", function(data)
if type(data) ~= "table" or type(data.id) ~= "string" or type(data.state) ~= "string" then
Bridge.Debug("error", "[sky_phone] Rejected invalid call state data.")
return
end
if data.state == "ringing" or data.state == "connected" then
active_call_payload = data
end
if data.state ~= "ringing" then
local focus_changed = call_focus
call_focus = false
if focus_changed then
update_nui_focus()
end
end
if data.state ~= "ringing" and data.state ~= "connected" then
active_call_payload = nil
end
if data.state == "connected" and data.channel then
if not join_call_voice(data.channel) then
TriggerEvent("sky_phone:device:error", "voice_unavailable")
end
elseif data.state ~= "ringing" then
leave_call_voice()
end
TriggerEvent("sky_phone:animation:call", data)
SendNUIMessage({ type = "call:state", data = data })
end)
CreateThread(function()
if Config.Phone.DevelopmentCommand then
TriggerEvent("chat:addSuggestion", "/" .. Config.Command, get_locale().CommandDescription)
end
if Config.TestData.Enabled then
TriggerEvent("chat:addSuggestion", "/" .. Config.TestData.Command, get_locale().TestData.CommandDescription)
end
end)
AddEventHandler("onResourceStop", function(resource_name)
if resource_name ~= GetCurrentResourceName() then
return
end
is_open = false
open_requested = false
notification_focus = false
call_focus = false
payphone_focus = false
camera_active = false
camera_nui_focused = true
sim_picker_open = false
sim_picker_payload = nil
active_call_payload = nil
activity_suspended = false
SetNuiFocusKeepInput(false)
SetNuiFocus(false, false)
TriggerEvent("sky_phone:animation:reset")
leave_call_voice()
if Config.Phone.DevelopmentCommand then
TriggerEvent("chat:removeSuggestion", "/" .. Config.Command)
end
if Config.TestData.Enabled then
TriggerEvent("chat:removeSuggestion", "/" .. Config.TestData.Command)
end
end)