mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
ENH - modernize SkyRide profile and interface
This commit is contained in:
@@ -565,7 +565,8 @@ const defaultLocales: LocaleTree = {
|
||||
camera: 'Camera',
|
||||
backToLogin: 'Back to CrewLink login',
|
||||
authErrors: {
|
||||
no_ifruit_account: 'Sign in to your Sky Cloud account in Settings first.',
|
||||
no_ifruit_account:
|
||||
'Sign in to your Sky Cloud account in Settings first.',
|
||||
invalid_username: 'Use 3–20 letters, numbers, dots, or underscores.',
|
||||
profile_not_found: 'No CrewLink profile exists for this iFruit email.',
|
||||
profile_exists: 'This iFruit email already has a CrewLink profile.',
|
||||
@@ -1190,7 +1191,8 @@ const defaultLocales: LocaleTree = {
|
||||
profileStep: 'Step 2 of 2',
|
||||
accountConnected: 'Sky Cloud account connected',
|
||||
authErrors: {
|
||||
no_ifruit_account: 'Sign in to your Sky Cloud account in Settings first.',
|
||||
no_ifruit_account:
|
||||
'Sign in to your Sky Cloud account in Settings first.',
|
||||
invalid_handle: 'Use 3–30 letters, numbers or underscores.',
|
||||
invalid_username: 'That username does not match your Feather profile.',
|
||||
profile_not_found: 'No Feather profile exists for this iFruit email.',
|
||||
@@ -2269,6 +2271,18 @@ const defaultLocales: LocaleTree = {
|
||||
acceptance: 'Acceptance',
|
||||
rating: 'Rating',
|
||||
notAvailable: 'N/A',
|
||||
profile: 'Your profile',
|
||||
editProfile: 'Edit Profile',
|
||||
editProfileBody: 'Change your public name and profile photo',
|
||||
profilePhoto: 'Profile photo',
|
||||
gallery: 'Gallery',
|
||||
camera: 'Camera',
|
||||
profileDetails: 'Profile details',
|
||||
profileName: 'Name',
|
||||
profileNamePlaceholder: 'Your SkyRide name',
|
||||
removeProfilePhoto: 'Remove profile photo',
|
||||
saveProfile: 'Save Profile',
|
||||
profileSaved: 'Your SkyRide profile was updated.',
|
||||
memberSince: 'SkyRide member since {date}',
|
||||
rideComplete: 'Ride Complete',
|
||||
rateRide: 'Rate your ride',
|
||||
@@ -2309,6 +2323,10 @@ const defaultLocales: LocaleTree = {
|
||||
invalid_rating: 'Choose a rating from one to five stars.',
|
||||
invalid_tip: 'Choose a valid tip amount.',
|
||||
invalid_comment: 'The rating comment is not valid.',
|
||||
invalid_profile: 'Choose a valid profile name and photo.',
|
||||
invalid_profile_name:
|
||||
'Your profile name must contain 2 to 50 characters.',
|
||||
invalid_profile_image: 'Choose a profile photo from your own gallery.',
|
||||
tip_failed: 'The tip could not be processed.',
|
||||
insufficient_funds:
|
||||
'Your selected payment method has insufficient funds.',
|
||||
@@ -2957,7 +2975,8 @@ const defaultLocales: LocaleTree = {
|
||||
'Review the listing and publish it when you are ready.',
|
||||
cityMarktPhotosHint: 'The CityMarkt listing photos will be included.',
|
||||
authErrors: {
|
||||
no_ifruit_account: 'Sign in to your Sky Cloud account in Settings first.',
|
||||
no_ifruit_account:
|
||||
'Sign in to your Sky Cloud account in Settings first.',
|
||||
invalid_username:
|
||||
'Use 3–24 lowercase letters, numbers, dots or underscores.',
|
||||
profile_not_found:
|
||||
|
||||
@@ -42,6 +42,7 @@ const bootstrap: SkyRideBootstrap = {
|
||||
pendingRating: null,
|
||||
profile: {
|
||||
acceptanceRate: 94,
|
||||
avatarMediaId: null,
|
||||
avatarUrl: null,
|
||||
cancelledRides: 2,
|
||||
completedRides: 128,
|
||||
@@ -112,6 +113,33 @@ describe('SkyRide store', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('updates the editable profile with an owned media id', async () => {
|
||||
const updatedProfile = {
|
||||
...bootstrap.profile,
|
||||
avatarMediaId: 17,
|
||||
avatarUrl: 'https://example.test/avatar.webp',
|
||||
name: 'Jordan Sky',
|
||||
}
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { profile: updatedProfile },
|
||||
success: true,
|
||||
})
|
||||
const skyride = useSkyRideStore()
|
||||
skyride.profile = bootstrap.profile
|
||||
|
||||
const response = await skyride.updateProfile({
|
||||
avatarMediaId: 17,
|
||||
name: 'Jordan Sky',
|
||||
})
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(skyride.profile).toEqual(updatedProfile)
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('skyride:update-profile', {
|
||||
avatarMediaId: 17,
|
||||
name: 'Jordan Sky',
|
||||
})
|
||||
})
|
||||
|
||||
it('books with the opaque quote id and never sends a client price', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { activeRide: ride },
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
SkyRideQuote,
|
||||
SkyRideQuoteOption,
|
||||
SkyRideRide,
|
||||
SkyRideProfileInput,
|
||||
SkyRideStateUpdate,
|
||||
} from '@/types/skyride'
|
||||
import { nuiCall, type NuiResponse } from '@/utils/nui'
|
||||
@@ -73,6 +74,23 @@ export const useSkyRideStore = defineStore('skyride', {
|
||||
this.error = ''
|
||||
return true
|
||||
},
|
||||
async updateProfile(
|
||||
profile: SkyRideProfileInput,
|
||||
): Promise<NuiResponse<SkyRideStateUpdate>> {
|
||||
this.isActionPending = true
|
||||
const response = await nuiCall<SkyRideStateUpdate>(
|
||||
'skyride:update-profile',
|
||||
profile,
|
||||
)
|
||||
this.isActionPending = false
|
||||
if (response.success) {
|
||||
if (response.data) this.applyUpdate(response.data)
|
||||
this.error = ''
|
||||
} else {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
}
|
||||
return response
|
||||
},
|
||||
async createQuote(
|
||||
pickup: SkyRideLocation,
|
||||
destination: SkyRideLocation,
|
||||
|
||||
@@ -100,6 +100,7 @@ export type SkyRideRide = {
|
||||
|
||||
export type SkyRideProfile = {
|
||||
acceptanceRate: number | null
|
||||
avatarMediaId: number | null
|
||||
avatarUrl: string | null
|
||||
cancelledRides: number
|
||||
completedRides: number
|
||||
@@ -112,6 +113,11 @@ export type SkyRideProfile = {
|
||||
rating: number
|
||||
}
|
||||
|
||||
export type SkyRideProfileInput = {
|
||||
avatarMediaId: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type SkyRideBootstrap = {
|
||||
activeRide: SkyRideRide | null
|
||||
availableRequests: SkyRideRide[]
|
||||
|
||||
@@ -10,6 +10,7 @@ const props = withDefaults(
|
||||
backLabel?: string
|
||||
showBack?: boolean
|
||||
showBackText?: boolean
|
||||
subtitle?: string
|
||||
title: string
|
||||
variant?: 'compact' | 'large'
|
||||
}>(),
|
||||
@@ -18,6 +19,7 @@ const props = withDefaults(
|
||||
backLabel: '',
|
||||
showBack: false,
|
||||
showBackText: false,
|
||||
subtitle: '',
|
||||
variant: 'compact',
|
||||
},
|
||||
)
|
||||
@@ -49,7 +51,10 @@ const accessibleBackLabel = computed(() => props.backLabel || props.title)
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<h1 class="sky-navbar__title">{{ title }}</h1>
|
||||
<div class="sky-navbar__heading">
|
||||
<h1 class="sky-navbar__title">{{ title }}</h1>
|
||||
<span v-if="subtitle" class="sky-navbar__subtitle">{{ subtitle }}</span>
|
||||
</div>
|
||||
|
||||
<div class="sky-navbar__right">
|
||||
<slot name="right" />
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyTabBar from '@/ui/SkyTabBar.vue'
|
||||
|
||||
describe('SkyTabBar', () => {
|
||||
it('keeps the docked tab bar as the default', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp(SkyTabBar, { label: 'Navigation' }),
|
||||
)
|
||||
|
||||
expect(html).toContain('class="sky-tabbar"')
|
||||
expect(html).not.toContain('sky-tabbar--floating')
|
||||
})
|
||||
|
||||
it('exposes the shared floating capsule variant', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp(SkyTabBar, { floating: true, label: 'Navigation' }),
|
||||
)
|
||||
|
||||
expect(html).toContain('sky-tabbar sky-tabbar--floating')
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
floating?: boolean
|
||||
label: string
|
||||
}>(),
|
||||
{
|
||||
floating: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-bind="$attrs" class="sky-tabbar" :aria-label="label">
|
||||
<nav
|
||||
v-bind="$attrs"
|
||||
class="sky-tabbar"
|
||||
:class="{ 'sky-tabbar--floating': floating }"
|
||||
:aria-label="label"
|
||||
>
|
||||
<div class="sky-tabbar__inner">
|
||||
<div class="sky-tabbar__pane">
|
||||
<slot />
|
||||
|
||||
@@ -218,6 +218,36 @@
|
||||
padding: var(--sky-space-4, 16px);
|
||||
}
|
||||
|
||||
.sky-button--small {
|
||||
padding-inline: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sky-card__header,
|
||||
.sky-card__footer {
|
||||
min-width: 0;
|
||||
padding: var(--sky-space-3, 12px) var(--sky-space-4, 16px);
|
||||
}
|
||||
|
||||
.sky-card__header--divider {
|
||||
border-bottom: 1px solid var(--sky-hairline, rgba(15, 23, 42, 0.1));
|
||||
}
|
||||
|
||||
.sky-card__footer--divider {
|
||||
border-top: 1px solid var(--sky-hairline, rgba(15, 23, 42, 0.1));
|
||||
}
|
||||
|
||||
.sky-block-header {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sky-block-header--inset {
|
||||
margin-inline: var(--sky-page-gutter, 14px);
|
||||
}
|
||||
|
||||
.sky-status-card {
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
@@ -1082,6 +1112,21 @@ label.sky-list-item__row {
|
||||
color: var(--sky-app-accent, #3b82f6);
|
||||
}
|
||||
|
||||
.sky-tabbar--floating .sky-tab-button {
|
||||
min-height: 54px;
|
||||
gap: 2px;
|
||||
padding-block: 5px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sky-tabbar--floating .sky-tab-button--active {
|
||||
background: var(--sky-surface-muted, #e6e9ee);
|
||||
}
|
||||
|
||||
.sky-tabbar--floating .sky-tab-button:active:not(:disabled) {
|
||||
background: var(--sky-app-accent-soft, rgba(59, 130, 246, 0.14));
|
||||
}
|
||||
|
||||
.sky-tab-button:active:not(:disabled) {
|
||||
background: var(--sky-app-accent-soft, rgba(59, 130, 246, 0.14));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
component?: 'div' | 'header' | 'section'
|
||||
inset?: boolean
|
||||
}>(),
|
||||
{
|
||||
component: 'header',
|
||||
inset: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="$attrs"
|
||||
class="sky-block-header"
|
||||
:class="{ 'sky-block-header--inset': inset }"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
@@ -13,6 +13,7 @@ const props = withDefaults(
|
||||
large?: boolean
|
||||
outline?: boolean
|
||||
rounded?: boolean
|
||||
small?: boolean
|
||||
type?: 'button' | 'reset' | 'submit'
|
||||
variant?: 'danger' | 'plain' | 'primary' | 'secondary'
|
||||
}>(),
|
||||
@@ -25,6 +26,7 @@ const props = withDefaults(
|
||||
large: false,
|
||||
outline: false,
|
||||
rounded: false,
|
||||
small: false,
|
||||
type: 'button',
|
||||
variant: 'primary',
|
||||
},
|
||||
@@ -73,6 +75,7 @@ function handleClick(event: MouseEvent): void {
|
||||
'sky-button--large': large,
|
||||
'sky-button--outline': outline,
|
||||
'sky-button--rounded': rounded,
|
||||
'sky-button--small': small,
|
||||
},
|
||||
]"
|
||||
@click="handleClick"
|
||||
|
||||
@@ -1,23 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from 'vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
component?: 'article' | 'div' | 'section'
|
||||
contentWrap?: boolean
|
||||
contentWrapPadding?: string
|
||||
footerDivider?: boolean
|
||||
headerDivider?: boolean
|
||||
}>(),
|
||||
{
|
||||
component: 'div',
|
||||
contentWrap: true,
|
||||
contentWrapPadding: '',
|
||||
footerDivider: false,
|
||||
headerDivider: false,
|
||||
},
|
||||
)
|
||||
|
||||
const slots = useSlots()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="component" v-bind="$attrs" class="sky-card">
|
||||
<div v-if="contentWrap" class="sky-card__content">
|
||||
<div
|
||||
v-if="slots.header"
|
||||
class="sky-card__header"
|
||||
:class="{ 'sky-card__header--divider': headerDivider }"
|
||||
>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div
|
||||
v-if="contentWrap"
|
||||
class="sky-card__content"
|
||||
:class="contentWrapPadding"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<slot v-else />
|
||||
<div
|
||||
v-if="slots.footer"
|
||||
class="sky-card__footer"
|
||||
:class="{ 'sky-card__footer--divider': footerDivider }"
|
||||
>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as SkyBadge } from './SkyBadge.vue'
|
||||
export { default as SkyBlock } from './SkyBlock.vue'
|
||||
export { default as SkyBlockHeader } from './SkyBlockHeader.vue'
|
||||
export { default as SkyBlockTitle } from './SkyBlockTitle.vue'
|
||||
export { default as SkyButton } from './SkyButton.vue'
|
||||
export { default as SkyCard } from './SkyCard.vue'
|
||||
|
||||
Vendored
+47
-2
@@ -53,8 +53,15 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sky-navbar__title {
|
||||
.sky-navbar__heading {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sky-navbar__title {
|
||||
margin: 0;
|
||||
padding: 0 var(--sky-space-1);
|
||||
overflow: hidden;
|
||||
@@ -67,6 +74,17 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sky-navbar__subtitle {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--sky-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sky-navbar__back {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
@@ -135,8 +153,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sky-navbar--large .sky-navbar__title {
|
||||
.sky-navbar--large .sky-navbar__heading {
|
||||
grid-area: title;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.sky-navbar--large .sky-navbar__title {
|
||||
padding: 0;
|
||||
font-size: var(--sky-font-large-title);
|
||||
font-weight: 700;
|
||||
@@ -403,6 +425,29 @@
|
||||
color: var(--sky-text);
|
||||
}
|
||||
|
||||
.sky-tabbar--floating {
|
||||
right: var(--sky-space-4);
|
||||
bottom: calc(var(--sky-safe-area-bottom) + 10px);
|
||||
left: var(--sky-space-4);
|
||||
min-height: 62px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--sky-hairline);
|
||||
border-radius: 999px;
|
||||
background: var(--sky-surface);
|
||||
box-shadow:
|
||||
0 12px 28px rgba(0, 0, 0, 0.34),
|
||||
inset 0 1px rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.sky-tabbar--floating .sky-tabbar__inner {
|
||||
min-height: 60px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.sky-tabbar--floating .sky-tabbar__pane {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.sky-tabbar__inner,
|
||||
.sky-tabbar__pane {
|
||||
width: 100%;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -888,6 +888,7 @@ let mockGarageValet = null
|
||||
|
||||
const skyRideProfile = {
|
||||
acceptanceRate: 96,
|
||||
avatarMediaId: 1,
|
||||
avatarUrl:
|
||||
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=240&q=80',
|
||||
cancelledRides: 3,
|
||||
@@ -6641,6 +6642,33 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: true, data: { items: skyRideHistory } })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:update-profile') {
|
||||
const name = String(request.body.name ?? '').trim()
|
||||
const avatarMediaId = Number(request.body.avatarMediaId)
|
||||
const avatar = avatarMediaId
|
||||
? mockMedia.find(
|
||||
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
|
||||
)
|
||||
: null
|
||||
if (
|
||||
name.length < 2 ||
|
||||
name.length > 50 ||
|
||||
!Number.isInteger(avatarMediaId) ||
|
||||
avatarMediaId < 0 ||
|
||||
(avatarMediaId > 0 && !avatar)
|
||||
) {
|
||||
response.json({ success: false, error: 'invalid_profile' })
|
||||
return
|
||||
}
|
||||
skyRideProfile.name = name
|
||||
skyRideProfile.avatarMediaId = avatarMediaId || null
|
||||
skyRideProfile.avatarUrl = avatar?.url ?? null
|
||||
response.json({
|
||||
success: true,
|
||||
data: skyRideUpdate(['profile']),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:quote') {
|
||||
const pickup = request.body.pickup
|
||||
const destination = request.body.destination
|
||||
@@ -7679,8 +7707,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
}
|
||||
}
|
||||
const bootstrapDeviceData =
|
||||
testScenario === 'easyshare-full' &&
|
||||
!deviceData.apps?.payload?.homeLayout
|
||||
testScenario === 'easyshare-full' && !deviceData.apps?.payload?.homeLayout
|
||||
? {
|
||||
...deviceData,
|
||||
apps: {
|
||||
|
||||
@@ -489,6 +489,8 @@ Config.Calendar = {
|
||||
Config.SkyRide = {
|
||||
PaymentAccount = "bank",
|
||||
Currency = "$",
|
||||
ProfileNameMinLength = 2,
|
||||
ProfileNameMaxLength = 50,
|
||||
DistanceUnit = "kilometer", -- kilometer or mile
|
||||
QuoteLifetimeSeconds = 120,
|
||||
HistoryLimit = 50,
|
||||
|
||||
@@ -1084,6 +1084,10 @@ Locales["en"] = {
|
||||
safetyBody = "Help, trip sharing, and emergency information", driverMode = "Driver Mode",
|
||||
driverModeBody = "Accept ride requests and track your earnings", completedRides = "Completed",
|
||||
cancelledRides = "Cancelled", acceptance = "Acceptance", rating = "Rating", notAvailable = "N/A",
|
||||
profile = "Your profile", editProfile = "Edit Profile", editProfileBody = "Change your public name and profile photo",
|
||||
profilePhoto = "Profile photo", gallery = "Gallery", camera = "Camera", profileDetails = "Profile details",
|
||||
profileName = "Name", profileNamePlaceholder = "Your SkyRide name", removeProfilePhoto = "Remove profile photo",
|
||||
saveProfile = "Save Profile", profileSaved = "Your SkyRide profile was updated.",
|
||||
memberSince = "SkyRide member since {date}", rideComplete = "Ride Complete", rateRide = "Rate your ride",
|
||||
rateRideBody = "How was your trip? Your feedback helps every rider.", ratingValue = "{rating} stars",
|
||||
tip = "Add a tip", noTip = "No tip", comment = "Comment", commentPlaceholder = "Share an optional note...",
|
||||
@@ -1103,6 +1107,8 @@ Locales["en"] = {
|
||||
destination_too_far = "Move closer to the destination to complete the ride.", payout_failed = "The driver payout could not be completed.",
|
||||
refund_failed = "The ride refund could not be completed.", invalid_cancel_reason = "Choose a valid cancellation reason.",
|
||||
invalid_tip = "Choose a valid tip amount.", invalid_comment = "The rating comment is not valid.",
|
||||
invalid_profile = "Choose a valid profile name and photo.", invalid_profile_name = "Your profile name must contain 2 to 50 characters.",
|
||||
invalid_profile_image = "Choose a profile photo from your own gallery.",
|
||||
tip_failed = "The tip could not be processed.", insufficient_funds = "Your selected payment method has insufficient funds.",
|
||||
rate_limited = "Please wait a moment before trying again.", request_failed = "SkyRide could not complete the request.",
|
||||
device_not_open = "Open the phone again to use SkyRide.", device_not_owned = "This phone does not belong to this character.",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
local server_callbacks = {
|
||||
"skyride:bootstrap",
|
||||
"skyride:update-profile",
|
||||
"skyride:quote",
|
||||
"skyride:request",
|
||||
"skyride:set-driver-status",
|
||||
@@ -79,6 +80,9 @@ local function normalize_state(data)
|
||||
if data.profile.avatarUrl == false then
|
||||
data.profile.avatarUrl = json.null
|
||||
end
|
||||
if data.profile.avatarMediaId == false then
|
||||
data.profile.avatarMediaId = json.null
|
||||
end
|
||||
if data.profile.earningsToday == false then
|
||||
data.profile.earningsToday = json.null
|
||||
end
|
||||
|
||||
@@ -1851,12 +1851,20 @@ local schema = {
|
||||
characterSet = "ascii",
|
||||
collation = "ascii_bin",
|
||||
},
|
||||
{ name = "display_name", type = "VARCHAR(50) NULL" },
|
||||
{ name = "avatar_media_id", type = "BIGINT UNSIGNED NULL" },
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
uniqueKeys = {
|
||||
{ name = "uniq_sky_phone_skyride_owner", columns = "(`owner_identifier`)" },
|
||||
},
|
||||
indexes = {
|
||||
{ name = "idx_sky_phone_skyride_profile_avatar", columns = "(`avatar_media_id`)" },
|
||||
},
|
||||
foreignKeys = {
|
||||
{ column = "avatar_media_id", references = "`sky_phone_media` (`id`) ON DELETE SET NULL" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,7 +29,11 @@ end
|
||||
local ride_select = [[
|
||||
SELECT r.*,
|
||||
passenger.`owner_identifier` AS `passenger_identifier`,
|
||||
COALESCE(NULLIF(passenger.`display_name`, ''), r.`passenger_name`) AS `passenger_profile_name`,
|
||||
passenger_avatar.`url` AS `passenger_avatar_url`,
|
||||
driver.`owner_identifier` AS `driver_identifier`,
|
||||
COALESCE(NULLIF(driver.`display_name`, ''), r.`driver_name`) AS `driver_profile_name`,
|
||||
driver_avatar.`url` AS `driver_avatar_url`,
|
||||
UNIX_TIMESTAMP(r.`created_at`) AS `created_at_unix`,
|
||||
UNIX_TIMESTAMP(r.`updated_at`) AS `updated_at_unix`,
|
||||
UNIX_TIMESTAMP(r.`accepted_at`) AS `accepted_at_unix`,
|
||||
@@ -40,8 +44,12 @@ local ride_select = [[
|
||||
FROM `sky_phone_skyride_rides` r
|
||||
INNER JOIN `sky_phone_skyride_profiles` passenger
|
||||
ON passenger.`id` = r.`passenger_profile_id`
|
||||
LEFT JOIN `sky_phone_media` passenger_avatar
|
||||
ON passenger_avatar.`id` = passenger.`avatar_media_id`
|
||||
LEFT JOIN `sky_phone_skyride_profiles` driver
|
||||
ON driver.`id` = r.`driver_profile_id`
|
||||
LEFT JOIN `sky_phone_media` driver_avatar
|
||||
ON driver_avatar.`id` = driver.`avatar_media_id`
|
||||
]]
|
||||
|
||||
local function affected_rows(result)
|
||||
@@ -164,9 +172,12 @@ local function require_profile(source)
|
||||
SELECT UUID(), ?
|
||||
]], { identifier })
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT `id`, `owner_identifier`, UNIX_TIMESTAMP(`created_at`) AS `created_at_unix`
|
||||
FROM `sky_phone_skyride_profiles`
|
||||
WHERE `owner_identifier` = ?
|
||||
SELECT profile.`id`, profile.`owner_identifier`, profile.`display_name`,
|
||||
profile.`avatar_media_id`, avatar.`url` AS `avatar_url`,
|
||||
UNIX_TIMESTAMP(profile.`created_at`) AS `created_at_unix`
|
||||
FROM `sky_phone_skyride_profiles` profile
|
||||
LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = profile.`avatar_media_id`
|
||||
WHERE profile.`owner_identifier` = ?
|
||||
LIMIT 1
|
||||
]], { identifier })
|
||||
if not rows[1] then
|
||||
@@ -235,7 +246,8 @@ local function profile_snapshot(source, profile)
|
||||
local stats = rows[1] or {}
|
||||
return {
|
||||
acceptanceRate = false,
|
||||
avatarUrl = false,
|
||||
avatarMediaId = profile.avatar_media_id and tonumber(profile.avatar_media_id) or false,
|
||||
avatarUrl = profile.avatar_url or false,
|
||||
cancelledRides = tonumber(stats.cancelled_rides) or 0,
|
||||
completedRides = tonumber(stats.completed_rides) or 0,
|
||||
currency = Config.SkyRide.Currency,
|
||||
@@ -243,7 +255,7 @@ local function profile_snapshot(source, profile)
|
||||
earningsToday = driver_eligible(source) and (tonumber(stats.earnings_today) or 0) or false,
|
||||
id = profile.id,
|
||||
memberSince = tonumber(profile.created_at_unix) or os.time(),
|
||||
name = player_name(source),
|
||||
name = profile.display_name or player_name(source),
|
||||
rating = math.floor(((tonumber(stats.rating) or 5.0) * 100) + 0.5) / 100,
|
||||
}
|
||||
end
|
||||
@@ -338,9 +350,9 @@ local function online_source_for_identifier(identifier)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function person_dto(id, name, metrics)
|
||||
local function person_dto(id, name, avatar_url, metrics)
|
||||
return {
|
||||
avatarUrl = false,
|
||||
avatarUrl = avatar_url or false,
|
||||
id = id,
|
||||
name = name or "",
|
||||
rating = metrics and metrics.rating or 5.0,
|
||||
@@ -356,12 +368,18 @@ local function ride_dtos(rows)
|
||||
local status = row.status == "completing" and "in_progress" or row.status
|
||||
local passenger = person_dto(
|
||||
row.passenger_profile_id,
|
||||
row.passenger_name,
|
||||
row.passenger_profile_name,
|
||||
row.passenger_avatar_url,
|
||||
metrics[row.passenger_profile_id]
|
||||
)
|
||||
local driver = false
|
||||
if row.driver_profile_id then
|
||||
driver = person_dto(row.driver_profile_id, row.driver_name, metrics[row.driver_profile_id])
|
||||
driver = person_dto(
|
||||
row.driver_profile_id,
|
||||
row.driver_profile_name,
|
||||
row.driver_avatar_url,
|
||||
metrics[row.driver_profile_id]
|
||||
)
|
||||
driver.vehicle = {
|
||||
color = row.driver_vehicle_color or "",
|
||||
model = row.driver_vehicle_model or "",
|
||||
@@ -819,6 +837,55 @@ Bridge.Callbacks.Register("sky_phone:skyride:bootstrap", function(source)
|
||||
}
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:skyride:update-profile", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "skyride_action", Config.SkyRide.ActionsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local profile, error_response = require_profile(source)
|
||||
if not profile then
|
||||
return error_response
|
||||
end
|
||||
local name = type(data) == "table" and type(data.name) == "string"
|
||||
and data.name:match("^%s*(.-)%s*$") or nil
|
||||
local name_length = name and utf8.len(name) or nil
|
||||
local avatar_media_id = type(data) == "table" and tonumber(data.avatarMediaId) or nil
|
||||
if not name_length
|
||||
or name_length < Config.SkyRide.ProfileNameMinLength
|
||||
or name_length > Config.SkyRide.ProfileNameMaxLength
|
||||
then
|
||||
return { success = false, error = "invalid_profile_name" }
|
||||
end
|
||||
if not avatar_media_id or avatar_media_id < 0 or avatar_media_id ~= math.floor(avatar_media_id) then
|
||||
return { success = false, error = "invalid_profile_image" }
|
||||
end
|
||||
if avatar_media_id > 0
|
||||
and not SkyPhoneMedia.ResolveOwnedMedia(source, tostring(avatar_media_id), "photo")
|
||||
then
|
||||
return { success = false, error = "invalid_profile_image" }
|
||||
end
|
||||
local result, lock_error = run_locked("profile:" .. profile.id, function()
|
||||
local update = Bridge.Database.Query([[
|
||||
UPDATE `sky_phone_skyride_profiles`
|
||||
SET `display_name` = ?, `avatar_media_id` = NULLIF(?, 0)
|
||||
WHERE `id` = ? AND `owner_identifier` = ?
|
||||
]], { name, avatar_media_id, profile.id, profile.owner_identifier })
|
||||
if affected_rows(update) > 1 then
|
||||
error(("[sky_phone] SkyRide profile update affected multiple rows for '%s'."):format(profile.id))
|
||||
end
|
||||
local updated_profile = require_profile(source)
|
||||
local ride = active_ride(profile.id)
|
||||
if ride then
|
||||
push_participants(ride, { activeRide = true }, source)
|
||||
end
|
||||
push_available_requests(source)
|
||||
return {
|
||||
success = true,
|
||||
data = state_snapshot(source, updated_profile, { profile = true }),
|
||||
}
|
||||
end)
|
||||
return result or lock_error
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:skyride:history", function(source)
|
||||
if not SkyPhone.AllowOperation(source, "skyride_read", Config.SkyRide.ReadsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
@@ -995,7 +1062,7 @@ Bridge.Callbacks.Register("sky_phone:skyride:request", function(source, data)
|
||||
]], {
|
||||
ride_id,
|
||||
profile.id,
|
||||
player_name(source),
|
||||
profile.display_name or player_name(source),
|
||||
quote.option.serviceClass,
|
||||
quote.pickup.label,
|
||||
quote.pickup.coords.x,
|
||||
@@ -1232,7 +1299,7 @@ Bridge.Callbacks.Register("sky_phone:skyride:accept", function(source, data)
|
||||
WHERE `id` = ? AND `status` = 'searching' AND `driver_profile_id` IS NULL
|
||||
]], {
|
||||
profile.id,
|
||||
player_name(source),
|
||||
profile.display_name or player_name(source),
|
||||
vehicle.model,
|
||||
vehicle.color,
|
||||
vehicle.plate,
|
||||
|
||||
@@ -795,9 +795,13 @@ CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_moderation_audit` (
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_skyride_profiles` (
|
||||
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`owner_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`display_name` VARCHAR(50) NULL,
|
||||
`avatar_media_id` BIGINT UNSIGNED NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_sky_phone_skyride_owner` (`owner_identifier`)
|
||||
UNIQUE KEY `uniq_sky_phone_skyride_owner` (`owner_identifier`),
|
||||
KEY `idx_sky_phone_skyride_profile_avatar` (`avatar_media_id`),
|
||||
FOREIGN KEY (`avatar_media_id`) REFERENCES `sky_phone_media` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_skyride_rides` (
|
||||
|
||||
Reference in New Issue
Block a user