mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
ADD - implement EasyShare system
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useEasyShareStore } from '@/stores/easyshare'
|
||||
import type { EasySharePayload, EasyShareTransfer } from '@/types/easyshare'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() }))
|
||||
|
||||
const mockNuiCall = vi.mocked(nuiCall)
|
||||
const payload: EasySharePayload = {
|
||||
appId: 'notes',
|
||||
copyText: 'Meet at Mission Row.',
|
||||
id: 'note-1',
|
||||
kind: 'note',
|
||||
title: 'Meeting',
|
||||
}
|
||||
const incoming: EasyShareTransfer = {
|
||||
createdAt: Date.now(),
|
||||
direction: 'incoming',
|
||||
id: 'transfer-1',
|
||||
otherName: 'Mia Santos',
|
||||
payload,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
}
|
||||
|
||||
describe('easyshare store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockNuiCall.mockReset()
|
||||
})
|
||||
|
||||
it('opens with the selected share payload', () => {
|
||||
const easyShare = useEasyShareStore()
|
||||
easyShare.open(payload)
|
||||
|
||||
expect(easyShare.opened).toBe(true)
|
||||
expect(easyShare.payload).toEqual(payload)
|
||||
expect(easyShare.nearbyOpened).toBe(false)
|
||||
})
|
||||
|
||||
it('opens the acceptance view for an incoming request', () => {
|
||||
const easyShare = useEasyShareStore()
|
||||
easyShare.applyEvent({ transfer: incoming })
|
||||
|
||||
expect(easyShare.opened).toBe(true)
|
||||
expect(easyShare.nearbyOpened).toBe(true)
|
||||
expect(easyShare.incomingTransfer).toEqual(incoming)
|
||||
})
|
||||
|
||||
it('keeps progress server-driven and removes terminal transfers from pending', () => {
|
||||
const easyShare = useEasyShareStore()
|
||||
easyShare.applyEvent({ transfer: incoming })
|
||||
easyShare.applyEvent({
|
||||
transfer: { ...incoming, progress: 50, status: 'transferring' },
|
||||
})
|
||||
expect(easyShare.pending[0]?.progress).toBe(50)
|
||||
|
||||
easyShare.applyEvent({
|
||||
transfer: { ...incoming, progress: 100, status: 'completed' },
|
||||
})
|
||||
expect(easyShare.pending).toEqual([])
|
||||
expect(easyShare.history[0]?.status).toBe('completed')
|
||||
})
|
||||
|
||||
it('sends only the target and current payload when requesting a transfer', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({ data: incoming, success: true })
|
||||
const easyShare = useEasyShareStore()
|
||||
easyShare.open(payload)
|
||||
|
||||
await easyShare.request(41)
|
||||
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('easyshare:request', {
|
||||
payload,
|
||||
targetId: 41,
|
||||
})
|
||||
})
|
||||
|
||||
it('hands a prepared share message to the selected chat app once', () => {
|
||||
const easyShare = useEasyShareStore()
|
||||
easyShare.open({ ...payload, link: 'https://notes.sky/note-1' })
|
||||
|
||||
expect(easyShare.prepareChatDraft('messages', '5551234567')).toBe(true)
|
||||
expect(easyShare.consumeChatDraft('messages')).toEqual({
|
||||
appId: 'messages',
|
||||
body: 'Meet at Mission Row.\nhttps://notes.sky/note-1',
|
||||
targetId: '5551234567',
|
||||
})
|
||||
expect(easyShare.consumeChatDraft('messages')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps a share draft until a conversation is chosen in the destination app', () => {
|
||||
const easyShare = useEasyShareStore()
|
||||
easyShare.open(payload)
|
||||
|
||||
expect(easyShare.prepareChatDraft('darkchat')).toBe(true)
|
||||
expect(easyShare.consumeChatDraft('darkchat')).toEqual({
|
||||
appId: 'darkchat',
|
||||
body: 'Meet at Mission Row.',
|
||||
targetId: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,180 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
EasyShareBootstrap,
|
||||
EasyShareChatApp,
|
||||
EasyShareChatDraft,
|
||||
EasyShareEvent,
|
||||
EasySharePayload,
|
||||
EasyShareTarget,
|
||||
EasyShareTransfer,
|
||||
EasyShareVisibility,
|
||||
} from '@/types/easyshare'
|
||||
import { nuiCall, type NuiResponse } from '@/utils/nui'
|
||||
|
||||
export const useEasyShareStore = defineStore('easyshare', () => {
|
||||
const opened = ref(false)
|
||||
const payload = ref<EasySharePayload | null>(null)
|
||||
const chatDraft = ref<EasyShareChatDraft | null>(null)
|
||||
const targets = ref<EasyShareTarget[]>([])
|
||||
const history = ref<EasyShareTransfer[]>([])
|
||||
const pending = ref<EasyShareTransfer[]>([])
|
||||
const visibility = ref<EasyShareVisibility>('everyone')
|
||||
const loading = ref(false)
|
||||
const nearbyOpened = ref(false)
|
||||
const historyOpened = ref(false)
|
||||
const activeTransfer = ref<EasyShareTransfer | null>(null)
|
||||
const incomingTransfer = computed(
|
||||
() => pending.value.find((transfer) => transfer.direction === 'incoming') ?? null,
|
||||
)
|
||||
|
||||
function open(nextPayload: EasySharePayload): void {
|
||||
payload.value = nextPayload
|
||||
nearbyOpened.value = false
|
||||
historyOpened.value = false
|
||||
activeTransfer.value = null
|
||||
opened.value = true
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
opened.value = false
|
||||
nearbyOpened.value = false
|
||||
historyOpened.value = false
|
||||
activeTransfer.value = null
|
||||
payload.value = null
|
||||
}
|
||||
|
||||
function prepareChatDraft(
|
||||
appId: EasyShareChatApp,
|
||||
targetId: string | null = null,
|
||||
): boolean {
|
||||
if (!payload.value) return false
|
||||
const parts = [payload.value.copyText.trim(), payload.value.link?.trim()].filter(
|
||||
(part): part is string => Boolean(part),
|
||||
)
|
||||
chatDraft.value = {
|
||||
appId,
|
||||
body: [...new Set(parts)].join('\n'),
|
||||
targetId,
|
||||
}
|
||||
return Boolean(chatDraft.value.body)
|
||||
}
|
||||
|
||||
function consumeChatDraft(appId: EasyShareChatApp): EasyShareChatDraft | null {
|
||||
if (chatDraft.value?.appId !== appId) return null
|
||||
const draft = chatDraft.value
|
||||
chatDraft.value = null
|
||||
return draft
|
||||
}
|
||||
|
||||
async function bootstrap(): Promise<boolean> {
|
||||
loading.value = true
|
||||
const response = await nuiCall<EasyShareBootstrap>('easyshare:bootstrap')
|
||||
loading.value = false
|
||||
if (!response.success || !response.data) return false
|
||||
targets.value = response.data.targets
|
||||
history.value = response.data.history
|
||||
pending.value = response.data.pending
|
||||
visibility.value = response.data.visibility
|
||||
return true
|
||||
}
|
||||
|
||||
async function showNearby(): Promise<void> {
|
||||
nearbyOpened.value = true
|
||||
historyOpened.value = false
|
||||
await bootstrap()
|
||||
}
|
||||
|
||||
async function showHistory(): Promise<void> {
|
||||
historyOpened.value = true
|
||||
nearbyOpened.value = false
|
||||
await bootstrap()
|
||||
}
|
||||
|
||||
async function setVisibility(next: EasyShareVisibility): Promise<boolean> {
|
||||
const response = await nuiCall<{ visibility: EasyShareVisibility }>(
|
||||
'easyshare:set-visibility',
|
||||
{ visibility: next },
|
||||
)
|
||||
if (response.success && response.data) visibility.value = response.data.visibility
|
||||
return response.success
|
||||
}
|
||||
|
||||
async function request(targetId: number): Promise<NuiResponse<EasyShareTransfer>> {
|
||||
if (!payload.value) return { success: false, error: 'missing_payload' }
|
||||
const response = await nuiCall<EasyShareTransfer>('easyshare:request', {
|
||||
payload: payload.value,
|
||||
targetId,
|
||||
})
|
||||
if (response.success && response.data) activeTransfer.value = response.data
|
||||
return response
|
||||
}
|
||||
|
||||
async function respond(id: string, accepted: boolean): Promise<boolean> {
|
||||
const response = await nuiCall<EasyShareTransfer>('easyshare:respond', {
|
||||
accepted,
|
||||
id,
|
||||
})
|
||||
if (response.success && response.data) applyTransfer(response.data)
|
||||
return response.success
|
||||
}
|
||||
|
||||
async function cancel(id: string): Promise<boolean> {
|
||||
const response = await nuiCall<EasyShareTransfer>('easyshare:cancel', { id })
|
||||
if (response.success && response.data) applyTransfer(response.data)
|
||||
return response.success
|
||||
}
|
||||
|
||||
function applyTransfer(transfer: EasyShareTransfer): void {
|
||||
const pendingIndex = pending.value.findIndex((entry) => entry.id === transfer.id)
|
||||
if (['pending', 'transferring'].includes(transfer.status)) {
|
||||
if (pendingIndex >= 0) pending.value[pendingIndex] = transfer
|
||||
else pending.value.unshift(transfer)
|
||||
} else if (pendingIndex >= 0) {
|
||||
pending.value.splice(pendingIndex, 1)
|
||||
}
|
||||
|
||||
const historyIndex = history.value.findIndex((entry) => entry.id === transfer.id)
|
||||
if (historyIndex >= 0) history.value[historyIndex] = transfer
|
||||
else history.value.unshift(transfer)
|
||||
history.value = history.value.slice(0, 50)
|
||||
if (activeTransfer.value?.id === transfer.id) activeTransfer.value = transfer
|
||||
}
|
||||
|
||||
function applyEvent(event: EasyShareEvent): void {
|
||||
applyTransfer(event.transfer)
|
||||
if (event.transfer.direction === 'incoming' && event.transfer.status === 'pending') {
|
||||
opened.value = true
|
||||
nearbyOpened.value = true
|
||||
historyOpened.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeTransfer,
|
||||
applyEvent,
|
||||
bootstrap,
|
||||
cancel,
|
||||
chatDraft,
|
||||
close,
|
||||
consumeChatDraft,
|
||||
history,
|
||||
historyOpened,
|
||||
incomingTransfer,
|
||||
loading,
|
||||
nearbyOpened,
|
||||
open,
|
||||
opened,
|
||||
payload,
|
||||
pending,
|
||||
prepareChatDraft,
|
||||
request,
|
||||
respond,
|
||||
setVisibility,
|
||||
showHistory,
|
||||
showNearby,
|
||||
targets,
|
||||
visibility,
|
||||
}
|
||||
})
|
||||
@@ -40,6 +40,56 @@ const namespaceQueues = new Map<string, Promise<void>>()
|
||||
|
||||
const defaultLocales: LocaleTree = {
|
||||
Apps: {
|
||||
easyShare: {
|
||||
name: 'EasyShare',
|
||||
incoming: 'Incoming Share',
|
||||
recentChats: 'Contacts and Chats',
|
||||
destinations: 'Share destinations',
|
||||
newMessage: 'New Message',
|
||||
shareProfile: 'Share Profile',
|
||||
chooseConversation: 'Choose a conversation',
|
||||
sentToChat: 'Sent to chat.',
|
||||
savedToNotes: 'Saved to Notes.',
|
||||
copy: 'Copy',
|
||||
copyLink: 'Copy Link',
|
||||
nearby: 'People Nearby',
|
||||
history: 'Transfer History',
|
||||
noHistory: 'No transfers yet.',
|
||||
noNearby: 'No visible players are nearby.',
|
||||
visibility: 'Visibility',
|
||||
requestSent: 'Waiting for acceptance...',
|
||||
incomingFrom: '{name} wants to share',
|
||||
distance: '{distance} m away',
|
||||
cancel: 'Cancel',
|
||||
visibilityOptions: {
|
||||
everyone: 'Everyone',
|
||||
contacts: 'Contacts Only',
|
||||
hidden: 'Receiving Off',
|
||||
},
|
||||
status: {
|
||||
pending: 'Waiting for acceptance',
|
||||
transferring: 'Transferring',
|
||||
completed: 'Completed',
|
||||
declined: 'Declined',
|
||||
cancelled: 'Cancelled',
|
||||
expired: 'Expired',
|
||||
failed: 'Failed',
|
||||
accepted: 'Accepted',
|
||||
},
|
||||
errors: {
|
||||
disabled: 'EasyShare is disabled.',
|
||||
invalid_payload: 'This item cannot be shared.',
|
||||
invalid_target: 'Choose a valid recipient.',
|
||||
target_unavailable: 'That player is no longer available.',
|
||||
transfer_not_found: 'That transfer is no longer available.',
|
||||
too_far: 'The recipient is too far away.',
|
||||
not_owned: 'This item no longer belongs to this phone.',
|
||||
payload_too_large: 'This item is too large to share.',
|
||||
location_unavailable: 'Your location is unavailable.',
|
||||
rate_limited: 'Too many share requests. Try again shortly.',
|
||||
request_failed: 'EasyShare is temporarily unavailable.',
|
||||
},
|
||||
},
|
||||
crewlink: {
|
||||
name: 'CrewLink',
|
||||
connecting: 'Connecting your crew...',
|
||||
|
||||
Reference in New Issue
Block a user