mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
ADD - implement CrewLink location groups
This commit is contained in:
@@ -185,6 +185,16 @@ type BillingNotificationData = {
|
||||
text?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
type CrewLinkNotificationData = {
|
||||
actor?: string
|
||||
device?: PhoneNotificationDevicePayload
|
||||
groupName?: string
|
||||
kind?: 'invite' | 'member_joined' | 'ping' | 'role' | 'removed'
|
||||
pingLabel?: string
|
||||
text?: string
|
||||
title?: string
|
||||
}
|
||||
const REFERENCE_VIEWPORT_WIDTH = 1920
|
||||
const REFERENCE_VIEWPORT_HEIGHT = 1080
|
||||
const PHONE_BASE_SCALE = 0.69
|
||||
@@ -656,6 +666,28 @@ function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
}
|
||||
}
|
||||
notifications.show(notification)
|
||||
} else if (
|
||||
event.data?.type === 'crewlink:notification' &&
|
||||
event.data.data
|
||||
) {
|
||||
const data = event.data.data as CrewLinkNotificationData
|
||||
const notification: PhoneNotificationInput = {
|
||||
appId: 'crewlink',
|
||||
subtitle: data.groupName,
|
||||
text: data.text ?? phone.t('Apps.crewlink.notifications.default'),
|
||||
title: data.title ?? phone.t('Apps.crewlink.name'),
|
||||
}
|
||||
if (
|
||||
data.device &&
|
||||
(!phone.isOpen || data.device.imei !== phone.device?.imei)
|
||||
) {
|
||||
notification.device = {
|
||||
imei: data.device.imei,
|
||||
name: data.device.name,
|
||||
preferences: parsePhonePreferences(data.device.settings ?? null),
|
||||
}
|
||||
}
|
||||
notifications.show(notification)
|
||||
} else if (
|
||||
(event.data?.type === 'call:incoming' ||
|
||||
event.data?.type === 'call:state') &&
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="CrewLink">
|
||||
<defs>
|
||||
<linearGradient id="crewlink-bg" x1="16" y1="12" x2="112" y2="118" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#24e2f2"/>
|
||||
<stop offset="0.52" stop-color="#0a9bf5"/>
|
||||
<stop offset="1" stop-color="#5746e8"/>
|
||||
</linearGradient>
|
||||
<filter id="crewlink-glow" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feGaussianBlur stdDeviation="3.5" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="128" height="128" rx="29" fill="url(#crewlink-bg)"/>
|
||||
<path d="M20 78c8-29 24-44 44-44s36 15 44 44" fill="none" stroke="#fff" stroke-opacity=".25" stroke-width="7" stroke-linecap="round"/>
|
||||
<path d="M31 84c7-21 18-31 33-31s26 10 33 31" fill="none" stroke="#fff" stroke-opacity=".45" stroke-width="7" stroke-linecap="round"/>
|
||||
<circle cx="64" cy="78" r="13" fill="#fff" filter="url(#crewlink-glow)"/>
|
||||
<circle cx="34" cy="86" r="10" fill="#d8fbff"/>
|
||||
<circle cx="94" cy="86" r="10" fill="#d8fbff"/>
|
||||
<path d="M64 102c-11-11-18-20-18-31a18 18 0 1 1 36 0c0 11-7 20-18 31Z" fill="#fff" fill-opacity=".18" stroke="#fff" stroke-width="3"/>
|
||||
<circle cx="64" cy="71" r="5" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -150,6 +150,7 @@ describe('app registry', () => {
|
||||
'flare',
|
||||
'radio',
|
||||
'local-pages',
|
||||
'crewlink',
|
||||
'phone',
|
||||
'darkchat',
|
||||
'banking',
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
Music2,
|
||||
Feather,
|
||||
ReceiptText,
|
||||
UsersRound,
|
||||
} from 'lucide-vue-next'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
@@ -68,6 +69,7 @@ import picstagramIcon from '@/assets/img/app-icons/picstagram.webp'
|
||||
import skyRideIcon from '@/assets/img/app-icons/skyride.svg'
|
||||
import musicIcon from '@/assets/img/app-icons/music.svg'
|
||||
import featherIcon from '@/assets/img/app-icons/feather.svg'
|
||||
import crewLinkIcon from '@/assets/img/app-icons/crewlink.svg'
|
||||
import type {
|
||||
LaunchablePhoneAppDefinition,
|
||||
LaunchablePhoneAppId,
|
||||
@@ -187,6 +189,20 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
labelKey: 'Apps.localPages.name',
|
||||
route: '/apps/local-pages',
|
||||
},
|
||||
{
|
||||
category: 'social',
|
||||
component: markRaw(
|
||||
defineAsyncComponent(() => import('@/views/apps/CrewLinkApp.vue')),
|
||||
),
|
||||
dockOrder: null,
|
||||
gridOrder: 27,
|
||||
icon: markRaw(UsersRound),
|
||||
iconClass: 'app-icon--crewlink',
|
||||
iconImage: crewLinkIcon,
|
||||
id: 'crewlink',
|
||||
labelKey: 'Apps.crewlink.name',
|
||||
route: '/apps/crewlink',
|
||||
},
|
||||
{
|
||||
category: 'social',
|
||||
component: markRaw(
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
vi.mock('@/utils/nui', () => ({
|
||||
nuiCall: vi.fn(),
|
||||
}))
|
||||
|
||||
import { useCrewLinkStore } from '@/stores/crewlink'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
describe('CrewLink store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.mocked(nuiCall).mockReset()
|
||||
})
|
||||
|
||||
it('hydrates a profile and active group from bootstrap', async () => {
|
||||
vi.mocked(nuiCall).mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
activeGroup: {
|
||||
allowMemberPings: true,
|
||||
colour: 'cyan',
|
||||
id: 'group-1',
|
||||
isOwner: true,
|
||||
memberCount: 1,
|
||||
members: [],
|
||||
name: 'Night Shift',
|
||||
overheadAllowed: true,
|
||||
pings: [],
|
||||
role: 'owner',
|
||||
},
|
||||
groups: [],
|
||||
invitations: [],
|
||||
profile: {
|
||||
activeGroupId: 'group-1',
|
||||
id: 'profile-1',
|
||||
mapVisible: true,
|
||||
overheadVisible: false,
|
||||
username: 'Skyline',
|
||||
},
|
||||
},
|
||||
})
|
||||
const store = useCrewLinkStore()
|
||||
expect(await store.bootstrap()).toBe(true)
|
||||
expect(store.profile?.username).toBe('Skyline')
|
||||
expect(store.activeGroup?.name).toBe('Night Shift')
|
||||
})
|
||||
|
||||
it('keeps the server error from a rejected request', async () => {
|
||||
vi.mocked(nuiCall).mockResolvedValue({
|
||||
success: false,
|
||||
error: 'invalid_code',
|
||||
})
|
||||
const store = useCrewLinkStore()
|
||||
const response = await store.joinCode('BADCODE')
|
||||
expect(response.success).toBe(false)
|
||||
expect(store.error).toBe('invalid_code')
|
||||
})
|
||||
|
||||
it('applies live members without replacing group metadata', async () => {
|
||||
const store = useCrewLinkStore()
|
||||
store.activeGroup = {
|
||||
allowMemberPings: true,
|
||||
colour: 'blue',
|
||||
id: 'group-1',
|
||||
isOwner: false,
|
||||
memberCount: 2,
|
||||
members: [],
|
||||
name: 'Road Crew',
|
||||
overheadAllowed: true,
|
||||
pings: [],
|
||||
role: 'member',
|
||||
}
|
||||
vi.mocked(nuiCall).mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
members: [
|
||||
{
|
||||
id: 'profile-2',
|
||||
joinedAt: 1,
|
||||
mapVisible: true,
|
||||
online: true,
|
||||
overheadVisible: true,
|
||||
role: 'member',
|
||||
username: 'Nova',
|
||||
},
|
||||
],
|
||||
pings: [],
|
||||
},
|
||||
})
|
||||
expect(await store.refreshLive()).toBe(true)
|
||||
expect(store.activeGroup.name).toBe('Road Crew')
|
||||
expect(store.activeGroup.members[0].online).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,180 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import type {
|
||||
CrewLinkBootstrap,
|
||||
CrewLinkColour,
|
||||
CrewLinkGroup,
|
||||
CrewLinkLive,
|
||||
CrewLinkNearbyPlayer,
|
||||
CrewLinkPing,
|
||||
CrewLinkPingType,
|
||||
CrewLinkRole,
|
||||
} from '@/types/crewlink'
|
||||
import { nuiCall, type NuiResponse } from '@/utils/nui'
|
||||
|
||||
export const useCrewLinkStore = defineStore('crewlink', {
|
||||
state: () => ({
|
||||
activeGroup: null as CrewLinkGroup | null,
|
||||
error: '',
|
||||
groups: [] as CrewLinkBootstrap['groups'],
|
||||
invitations: [] as CrewLinkBootstrap['invitations'],
|
||||
isLoading: false,
|
||||
limits: null as CrewLinkBootstrap['limits'] | null,
|
||||
profile: null as CrewLinkBootstrap['profile'],
|
||||
}),
|
||||
actions: {
|
||||
applyBootstrap(data: CrewLinkBootstrap): void {
|
||||
this.profile = data.profile
|
||||
this.groups = data.groups ?? []
|
||||
this.activeGroup = data.activeGroup ?? null
|
||||
this.invitations = data.invitations ?? []
|
||||
this.limits = data.limits ?? null
|
||||
this.error = ''
|
||||
},
|
||||
async bootstrap(): Promise<boolean> {
|
||||
this.isLoading = true
|
||||
const response = await nuiCall<CrewLinkBootstrap>('crewlink:bootstrap')
|
||||
this.isLoading = false
|
||||
if (response.success && response.data) {
|
||||
this.applyBootstrap(response.data)
|
||||
return true
|
||||
}
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
},
|
||||
async request(
|
||||
endpoint: string,
|
||||
data: Record<string, unknown> = {},
|
||||
refresh = true,
|
||||
): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
this.isLoading = true
|
||||
const response = await nuiCall<CrewLinkBootstrap>(endpoint, data)
|
||||
this.isLoading = false
|
||||
if (!response.success) {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return response
|
||||
}
|
||||
this.error = ''
|
||||
if (response.data?.profile !== undefined) {
|
||||
this.applyBootstrap(response.data)
|
||||
} else if (refresh) {
|
||||
await this.bootstrap()
|
||||
}
|
||||
return response
|
||||
},
|
||||
createProfile(username: string): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:create-profile', { username })
|
||||
},
|
||||
updateProfile(
|
||||
username: string,
|
||||
mapVisible: boolean,
|
||||
overheadVisible: boolean,
|
||||
): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:update-profile', {
|
||||
mapVisible,
|
||||
overheadVisible,
|
||||
username,
|
||||
})
|
||||
},
|
||||
createGroup(
|
||||
name: string,
|
||||
colour: CrewLinkColour,
|
||||
): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:create-group', { colour, name })
|
||||
},
|
||||
updateGroup(
|
||||
groupId: string,
|
||||
name: string,
|
||||
colour: CrewLinkColour,
|
||||
allowMemberPings: boolean,
|
||||
overheadAllowed: boolean,
|
||||
): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:update-group', {
|
||||
allowMemberPings,
|
||||
colour,
|
||||
groupId,
|
||||
name,
|
||||
overheadAllowed,
|
||||
})
|
||||
},
|
||||
deleteGroup(groupId: string): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:delete-group', { groupId })
|
||||
},
|
||||
setActive(groupId: string): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:set-active', { groupId })
|
||||
},
|
||||
joinCode(code: string): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:join-code', { code })
|
||||
},
|
||||
rotateCode(groupId: string): Promise<NuiResponse<{ inviteCode: string }>> {
|
||||
return nuiCall<{ inviteCode: string }>('crewlink:rotate-code', { groupId })
|
||||
},
|
||||
nearby(): Promise<NuiResponse<CrewLinkNearbyPlayer[]>> {
|
||||
return nuiCall<CrewLinkNearbyPlayer[]>('crewlink:nearby')
|
||||
},
|
||||
inviteNearby(targetSource: number): Promise<NuiResponse> {
|
||||
return this.request(
|
||||
'crewlink:invite-nearby',
|
||||
{ targetSource },
|
||||
false,
|
||||
)
|
||||
},
|
||||
respondInvite(
|
||||
invitationId: string,
|
||||
accepted: boolean,
|
||||
): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:respond-invite', {
|
||||
accepted,
|
||||
invitationId,
|
||||
})
|
||||
},
|
||||
updateMember(
|
||||
groupId: string,
|
||||
profileId: string,
|
||||
role: CrewLinkRole,
|
||||
): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:update-member', {
|
||||
groupId,
|
||||
profileId,
|
||||
role,
|
||||
})
|
||||
},
|
||||
transferOwner(
|
||||
groupId: string,
|
||||
profileId: string,
|
||||
): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:transfer-owner', { groupId, profileId })
|
||||
},
|
||||
removeMember(
|
||||
groupId: string,
|
||||
profileId: string,
|
||||
): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:remove-member', { groupId, profileId })
|
||||
},
|
||||
leave(groupId: string): Promise<NuiResponse<CrewLinkBootstrap>> {
|
||||
return this.request('crewlink:leave', { groupId })
|
||||
},
|
||||
createPing(
|
||||
type: CrewLinkPingType,
|
||||
label: string,
|
||||
coords?: { x: number; y: number; z: number },
|
||||
): Promise<NuiResponse<CrewLinkPing>> {
|
||||
return nuiCall<CrewLinkPing>('crewlink:create-ping', {
|
||||
coords,
|
||||
label,
|
||||
type,
|
||||
useCurrent: !coords,
|
||||
})
|
||||
},
|
||||
removePing(pingId: string): Promise<NuiResponse> {
|
||||
return this.request('crewlink:remove-ping', { pingId })
|
||||
},
|
||||
async refreshLive(): Promise<boolean> {
|
||||
const response = await nuiCall<CrewLinkLive>('crewlink:live')
|
||||
if (!response.success || !response.data || !this.activeGroup) return false
|
||||
this.activeGroup.members = response.data.members
|
||||
this.activeGroup.pings = response.data.pings
|
||||
return true
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -40,6 +40,205 @@ const namespaceQueues = new Map<string, Promise<void>>()
|
||||
|
||||
const defaultLocales: LocaleTree = {
|
||||
Apps: {
|
||||
crewlink: {
|
||||
name: 'CrewLink',
|
||||
connecting: 'Connecting your crew...',
|
||||
privateNetwork: 'Private location network',
|
||||
signInTitle: 'Connect your iFruit Account',
|
||||
signInBody:
|
||||
'CrewLink uses your private iFruit identity to keep groups and roles available across your phones.',
|
||||
openSettings: 'Open iFruit Settings',
|
||||
welcomeEyebrow: 'Your crew. One signal.',
|
||||
welcomeTitle: 'Find your people',
|
||||
welcomeBody:
|
||||
'Choose a private CrewLink username. Only confirmed group members can see your presence and shared location.',
|
||||
username: 'CrewLink username',
|
||||
usernamePlaceholder: 'e.g. Skyline',
|
||||
createProfile: 'Create CrewLink Profile',
|
||||
profileCreated: 'Your CrewLink profile is ready.',
|
||||
privacyNote: 'Private by design. No public player search.',
|
||||
noGroupEyebrow: 'No active crew',
|
||||
noGroupTitle: 'Build your private network',
|
||||
noGroupBody:
|
||||
'Create a crew or join friends with a private invitation code.',
|
||||
createGroup: 'Create Group',
|
||||
createGroupBody:
|
||||
'Give your crew a recognizable name and signal colour.',
|
||||
joinGroup: 'Join Group',
|
||||
joinWithCode: 'Join with Code',
|
||||
joinWithCodeBody:
|
||||
'Enter the private eight-character code shared by a coordinator.',
|
||||
groupName: 'Group name',
|
||||
groupNamePlaceholder: 'e.g. Night Shift',
|
||||
groupColour: 'Signal colour',
|
||||
createCrew: 'Create Crew',
|
||||
joinCrew: 'Join Crew',
|
||||
inviteCode: 'Invitation code',
|
||||
inviteCodePlaceholder: 'AB12CD34',
|
||||
groupCreated: 'Crew created.',
|
||||
joinedGroup: 'You joined the crew.',
|
||||
activeGroupChanged: 'Active crew changed.',
|
||||
pendingInvitations: 'Invitations',
|
||||
invitedBy: 'Invited by @{username}',
|
||||
inviteAccepted: 'Invitation accepted.',
|
||||
inviteDeclined: 'Invitation declined.',
|
||||
navigation: 'CrewLink navigation',
|
||||
map: 'Map',
|
||||
crew: 'Crew',
|
||||
pings: 'Pings',
|
||||
profile: 'Profile',
|
||||
onlineNow: 'online now',
|
||||
zoomIn: 'Zoom in',
|
||||
zoomOut: 'Zoom out',
|
||||
myLocation: 'Center my location',
|
||||
member: 'Member',
|
||||
members: 'Members',
|
||||
openCrew: 'View team',
|
||||
newPing: 'New Ping',
|
||||
shareLocation: 'Mark a place',
|
||||
locationUnavailable: 'Your live location is currently hidden.',
|
||||
activeCrew: 'Active private crew',
|
||||
groupSummarySingle: '{online} of {total} member online',
|
||||
groupSummary: '{online} of {total} members online',
|
||||
private: 'Private',
|
||||
nearby: 'Nearby',
|
||||
copyCode: 'Copy code',
|
||||
manage: 'Manage',
|
||||
refresh: 'Refresh',
|
||||
codeCopied: 'Invitation code copied.',
|
||||
codeRotated: 'A new invitation code is active.',
|
||||
peopleNearby: 'People Nearby',
|
||||
peopleNearbyBody:
|
||||
'Only CrewLink users within {distance} meters appear here.',
|
||||
metersAway: '{distance} m away',
|
||||
invite: 'Invite',
|
||||
inviteSent: 'Invitation sent to @{username}.',
|
||||
nobodyNearby: 'Nobody in range',
|
||||
nobodyNearbyBody:
|
||||
'Move closer to another CrewLink user and scan again.',
|
||||
scanAgain: 'Scan Again',
|
||||
liveCoordination: 'Live coordination',
|
||||
noPings: 'No active pings',
|
||||
noPingsBody: 'Share a meeting point, warning, or target with your crew.',
|
||||
expiresMinutes: '{count} min left',
|
||||
expiresSeconds: '{count} sec left',
|
||||
sharedBy: 'Shared by @{username}',
|
||||
setRoute: 'Set Route',
|
||||
routeSet: 'GPS route set.',
|
||||
pingCreated: 'Ping shared with your crew.',
|
||||
pingRemoved: 'Ping removed.',
|
||||
newPingBody:
|
||||
'Share your current position or place the ping at the center of the map.',
|
||||
pingLabel: 'Ping label',
|
||||
pingLabelPlaceholder: 'e.g. Meet at the garage',
|
||||
placeOnMap: 'Use map center',
|
||||
placeOnMapBody: 'Otherwise your current position is used.',
|
||||
sharePing: 'Share Ping',
|
||||
pingTypes: {
|
||||
meeting: 'Meet',
|
||||
danger: 'Danger',
|
||||
help: 'Help',
|
||||
target: 'Target',
|
||||
info: 'Info',
|
||||
},
|
||||
status: { live: 'Live', hidden: 'Location hidden', offline: 'Offline' },
|
||||
roles: {
|
||||
owner: 'Owner',
|
||||
coordinator: 'Coordinator',
|
||||
moderator: 'Moderator',
|
||||
member: 'Member',
|
||||
guest: 'Guest',
|
||||
},
|
||||
roleDescriptions: {
|
||||
coordinator: 'Manage invitations, settings, roles, and pings.',
|
||||
moderator: 'Invite nearby users and moderate group pings.',
|
||||
member: 'Share presence and create pings when enabled.',
|
||||
guest: 'View the active crew without coordination permissions.',
|
||||
},
|
||||
manageCrew: 'Manage Crew',
|
||||
manageCrewBody: 'Update identity, permissions, and invitations.',
|
||||
memberPings: 'Member pings',
|
||||
memberPingsBody: 'Allow members and guests to create pings.',
|
||||
allowOverhead: 'Overhead labels',
|
||||
allowOverheadBody: 'Permit nearby opt-in member labels in the world.',
|
||||
groupSaved: 'Crew settings saved.',
|
||||
assignRole: 'Assign Role',
|
||||
roleUpdated: 'Member role updated.',
|
||||
transferOwnership: 'Transfer Ownership',
|
||||
removeMember: 'Remove from Crew',
|
||||
yourCrewLinkId: 'Your CrewLink ID',
|
||||
privacyVisibility: 'Privacy & Visibility',
|
||||
shareOnMap: 'Share on group map',
|
||||
shareOnMapBody: 'Active members can see your live position.',
|
||||
overheadLabels: 'Nearby overhead label',
|
||||
overheadLabelsBody: 'Show your CrewLink name to nearby opted-in members.',
|
||||
yourGroups: 'Your Groups',
|
||||
createAnotherGroup: 'Create another group',
|
||||
account: 'CrewLink Account',
|
||||
externalApi: 'Connected resources',
|
||||
externalApiBody: 'Approved scripts may add temporary group pings.',
|
||||
editUsername: 'Edit Username',
|
||||
editUsernameBody: 'Your username is unique across CrewLink.',
|
||||
profileSaved: 'CrewLink profile saved.',
|
||||
deleteGroup: 'Delete Group',
|
||||
leaveGroup: 'Leave Group',
|
||||
confirmAction: 'Confirm',
|
||||
confirm: {
|
||||
'delete-group': {
|
||||
title: 'Delete this crew?',
|
||||
body: 'The group, memberships, invitations, and active pings will be permanently removed.',
|
||||
},
|
||||
'leave-group': {
|
||||
title: 'Leave this crew?',
|
||||
body: 'You will lose access to its members, map, and pings.',
|
||||
},
|
||||
'remove-member': {
|
||||
title: 'Remove this member?',
|
||||
body: 'They will immediately lose access to this crew.',
|
||||
},
|
||||
'transfer-owner': {
|
||||
title: 'Transfer ownership?',
|
||||
body: 'The selected member becomes owner and you become coordinator.',
|
||||
},
|
||||
},
|
||||
'delete-groupDone': 'Crew deleted.',
|
||||
'leave-groupDone': 'You left the crew.',
|
||||
'remove-memberDone': 'Member removed.',
|
||||
'transfer-ownerDone': 'Ownership transferred.',
|
||||
notifications: {
|
||||
invite: '{actor} invited you to {group}.',
|
||||
member_joined: '{actor} joined {group}.',
|
||||
ping: '{actor} shared “{ping}” with {group}.',
|
||||
role: 'Your CrewLink role changed in {group}.',
|
||||
removed: 'You were removed from {group}.',
|
||||
default: 'Your CrewLink group has an update.',
|
||||
},
|
||||
errors: {
|
||||
not_authenticated: 'Sign in to your iFruit Account first.',
|
||||
profile_required: 'Create your CrewLink profile first.',
|
||||
invalid_username: 'Use 3–20 letters, numbers, dots, or underscores.',
|
||||
invalid_profile: 'Check your profile details.',
|
||||
username_taken: 'That CrewLink username is already taken.',
|
||||
invalid_group: 'Choose a valid group name and signal colour.',
|
||||
group_limit: 'You have reached your group limit.',
|
||||
member_limit: 'This group has reached its member limit.',
|
||||
group_not_found: 'This group is no longer available.',
|
||||
invalid_code: 'That invitation code is invalid.',
|
||||
already_member: 'You are already a member of this group.',
|
||||
forbidden: 'Your current role cannot perform this action.',
|
||||
player_not_nearby: 'That player is no longer nearby.',
|
||||
player_unavailable: 'That player cannot receive a CrewLink invitation.',
|
||||
invalid_invitation: 'This invitation is invalid.',
|
||||
invitation_expired: 'This invitation has expired.',
|
||||
invalid_role: 'That role cannot be assigned.',
|
||||
owner_must_transfer: 'Transfer ownership before leaving this group.',
|
||||
invalid_ping: 'Choose a valid ping type, label, and position.',
|
||||
ping_limit: 'This crew already has too many active pings.',
|
||||
ping_not_found: 'That ping is no longer active.',
|
||||
rate_limited: 'Too many CrewLink actions. Try again shortly.',
|
||||
request_failed: 'CrewLink is temporarily unavailable.',
|
||||
},
|
||||
},
|
||||
flare: {
|
||||
name: 'Flare',
|
||||
signInTitle: 'Sign in to Flare',
|
||||
|
||||
@@ -35,6 +35,7 @@ export type PhoneAppId =
|
||||
| 'picstagram'
|
||||
| 'skyride'
|
||||
| 'feather'
|
||||
| 'crewlink'
|
||||
|
||||
export type LaunchablePhoneAppId = PhoneAppId
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { MapPoint } from '@/features/map/defaultMapGeometry'
|
||||
|
||||
export type CrewLinkRole =
|
||||
| 'owner'
|
||||
| 'coordinator'
|
||||
| 'moderator'
|
||||
| 'member'
|
||||
| 'guest'
|
||||
|
||||
export type CrewLinkColour =
|
||||
| 'cyan'
|
||||
| 'blue'
|
||||
| 'violet'
|
||||
| 'orange'
|
||||
| 'green'
|
||||
| 'rose'
|
||||
|
||||
export type CrewLinkPingType =
|
||||
| 'meeting'
|
||||
| 'danger'
|
||||
| 'help'
|
||||
| 'target'
|
||||
| 'info'
|
||||
|
||||
export type CrewLinkProfile = {
|
||||
activeGroupId: string | null
|
||||
id: string
|
||||
mapVisible: boolean
|
||||
overheadVisible: boolean
|
||||
username: string
|
||||
}
|
||||
|
||||
export type CrewLinkMember = {
|
||||
coords?: MapPoint & { z: number }
|
||||
id: string
|
||||
joinedAt: number
|
||||
mapVisible: boolean
|
||||
online: boolean
|
||||
overheadVisible: boolean
|
||||
role: CrewLinkRole
|
||||
source?: number
|
||||
username: string
|
||||
}
|
||||
|
||||
export type CrewLinkPing = {
|
||||
coords: MapPoint & { z: number }
|
||||
createdAt: number
|
||||
creatorProfileId: string | null
|
||||
creatorUsername: string
|
||||
expiresAt: number
|
||||
id: string
|
||||
label: string
|
||||
sourceResource?: string | null
|
||||
type: CrewLinkPingType
|
||||
}
|
||||
|
||||
export type CrewLinkGroupSummary = {
|
||||
allowMemberPings: boolean
|
||||
colour: CrewLinkColour
|
||||
id: string
|
||||
inviteCode?: string
|
||||
isOwner: boolean
|
||||
memberCount: number
|
||||
name: string
|
||||
overheadAllowed: boolean
|
||||
role: CrewLinkRole
|
||||
}
|
||||
|
||||
export type CrewLinkGroup = CrewLinkGroupSummary & {
|
||||
members: CrewLinkMember[]
|
||||
pings: CrewLinkPing[]
|
||||
}
|
||||
|
||||
export type CrewLinkInvitation = {
|
||||
colour: CrewLinkColour
|
||||
expiresAt: number
|
||||
groupId: string
|
||||
groupName: string
|
||||
id: string
|
||||
inviterUsername: string
|
||||
}
|
||||
|
||||
export type CrewLinkLimits = {
|
||||
maximumGroups: number
|
||||
maximumMembers: number
|
||||
nearbyDistance: number
|
||||
overheadDistance: number
|
||||
pingLifetimeSeconds: number
|
||||
}
|
||||
|
||||
export type CrewLinkBootstrap = {
|
||||
activeGroup?: CrewLinkGroup | null
|
||||
groups: CrewLinkGroupSummary[]
|
||||
invitations: CrewLinkInvitation[]
|
||||
limits?: CrewLinkLimits
|
||||
profile: CrewLinkProfile | null
|
||||
}
|
||||
|
||||
export type CrewLinkNearbyPlayer = {
|
||||
distance: number
|
||||
source: number
|
||||
username: string
|
||||
}
|
||||
|
||||
export type CrewLinkLive = {
|
||||
members: CrewLinkMember[]
|
||||
pings: CrewLinkPing[]
|
||||
}
|
||||
@@ -84,6 +84,7 @@ const DEFAULT_APP_NOTIFICATIONS: Record<
|
||||
picstagram: { enabled: true, sounds: true },
|
||||
fliptok: { enabled: true, sounds: true },
|
||||
feather: { enabled: true, sounds: true },
|
||||
crewlink: { enabled: true, sounds: true },
|
||||
camera: { enabled: true, sounds: true },
|
||||
clock: { enabled: true, sounds: true },
|
||||
calendar: { enabled: true, sounds: true },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user