mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
ENH - complete Mail and Radio Sky UI
This commit is contained in:
@@ -18,6 +18,7 @@ const counts: MailCounts = {
|
||||
trash: 4,
|
||||
unread: 1,
|
||||
}
|
||||
const noMailboxes = { mailboxes: [] }
|
||||
|
||||
function listItem(id: number): MailListItem {
|
||||
return {
|
||||
@@ -161,6 +162,7 @@ describe('mail store', () => {
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({ data: counts, success: true })
|
||||
.mockResolvedValueOnce({ data: noMailboxes, success: true })
|
||||
.mockResolvedValueOnce({
|
||||
data: { hasMore: true, items: [listItem(1)] },
|
||||
success: true,
|
||||
@@ -183,6 +185,7 @@ describe('mail store', () => {
|
||||
it('removes loaded cloud mail when the device becomes unlinked', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({ data: counts, success: true })
|
||||
.mockResolvedValueOnce({ data: noMailboxes, success: true })
|
||||
.mockResolvedValueOnce({
|
||||
data: { hasMore: true, items: [listItem(1)] },
|
||||
success: true,
|
||||
@@ -234,6 +237,10 @@ describe('mail store', () => {
|
||||
resolveCounts = resolve
|
||||
}),
|
||||
)
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: noMailboxes,
|
||||
success: true,
|
||||
})
|
||||
const mail = useMailStore()
|
||||
|
||||
const bootstrap = mail.bootstrap('alex@ifruit.com')
|
||||
@@ -282,6 +289,7 @@ describe('mail store', () => {
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce({ data: counts, success: true })
|
||||
.mockResolvedValueOnce({ data: noMailboxes, success: true })
|
||||
const mail = useMailStore()
|
||||
const account = useAccountStore()
|
||||
|
||||
@@ -297,4 +305,92 @@ describe('mail store', () => {
|
||||
expect(mail.accountEmail).toBe('morgan@ifruit.com')
|
||||
expect(account.email).toBe('morgan@ifruit.com')
|
||||
})
|
||||
|
||||
it('creates an account-owned mailbox and refreshes the mailbox list', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({
|
||||
data: { count: 0, id: 7, name: 'Projects', sort_order: 1 },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
mailboxes: [{ count: 0, id: 7, name: 'Projects', sort_order: 1 }],
|
||||
},
|
||||
success: true,
|
||||
})
|
||||
const mail = useMailStore()
|
||||
|
||||
const response = await mail.createMailbox('Projects')
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(mockNuiCall).toHaveBeenNthCalledWith(1, 'mail:create-mailbox', {
|
||||
name: 'Projects',
|
||||
})
|
||||
expect(mockNuiCall).toHaveBeenNthCalledWith(2, 'mail:mailboxes')
|
||||
expect(mail.mailboxes).toEqual([
|
||||
{ count: 0, id: 7, name: 'Projects', sort_order: 1 },
|
||||
])
|
||||
})
|
||||
|
||||
it('loads custom mailbox folders through the regular list contract', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { hasMore: false, items: [listItem(4)] },
|
||||
success: true,
|
||||
})
|
||||
const mail = useMailStore()
|
||||
|
||||
expect(await mail.loadFolder('mailbox:7')).toBe(true)
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('mail:list', {
|
||||
folder: 'mailbox:7',
|
||||
offset: 0,
|
||||
search: '',
|
||||
})
|
||||
expect(mail.folder).toBe('mailbox:7')
|
||||
})
|
||||
|
||||
it('keeps active mail filters in the paginated server request', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { hasMore: false, items: [listItem(5)] },
|
||||
success: true,
|
||||
})
|
||||
const mail = useMailStore()
|
||||
mail.setListFilters({
|
||||
address: 'to-me',
|
||||
direction: 'all',
|
||||
read: 'unread',
|
||||
today: true,
|
||||
})
|
||||
|
||||
expect(await mail.loadFolder('inbox')).toBe(true)
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('mail:list', {
|
||||
filters: {
|
||||
address: 'to-me',
|
||||
direction: 'all',
|
||||
read: 'unread',
|
||||
today: true,
|
||||
},
|
||||
folder: 'inbox',
|
||||
offset: 0,
|
||||
search: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('moves a custom mailbox entry back to its original built-in folder', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
.mockResolvedValueOnce({ data: counts, success: true })
|
||||
.mockResolvedValueOnce({ data: noMailboxes, success: true })
|
||||
.mockResolvedValueOnce({
|
||||
data: { hasMore: false, items: [] },
|
||||
success: true,
|
||||
})
|
||||
const mail = useMailStore()
|
||||
mail.folder = 'mailbox:7'
|
||||
|
||||
expect(await mail.moveEntry(4, null)).toBe(true)
|
||||
expect(mockNuiCall).toHaveBeenNthCalledWith(1, 'mail:move', {
|
||||
id: 4,
|
||||
mailboxId: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,9 +7,12 @@ import type {
|
||||
MailComposeDraft,
|
||||
MailCounts,
|
||||
MailDraft,
|
||||
MailFolder,
|
||||
MailFolderKey,
|
||||
MailListItem,
|
||||
MailListFilters,
|
||||
MailListResponse,
|
||||
MailMailbox,
|
||||
MailMailboxesResponse,
|
||||
MailMessage,
|
||||
} from '@/types/mail'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
@@ -21,15 +24,23 @@ const emptyCounts = (): MailCounts => ({
|
||||
trash: 0,
|
||||
unread: 0,
|
||||
})
|
||||
const emptyListFilters = (): MailListFilters => ({
|
||||
address: 'all',
|
||||
direction: 'all',
|
||||
read: 'all',
|
||||
today: false,
|
||||
})
|
||||
|
||||
export const useMailStore = defineStore('mail', () => {
|
||||
const account = useAccountStore()
|
||||
const accountEmail = ref('')
|
||||
const counts = ref<MailCounts>(emptyCounts())
|
||||
const folder = ref<MailFolder>('inbox')
|
||||
const folder = ref<MailFolderKey>('inbox')
|
||||
const hasMore = ref(false)
|
||||
const items = ref<MailListItem[]>([])
|
||||
const loading = ref(false)
|
||||
const listFilters = ref<MailListFilters>(emptyListFilters())
|
||||
const mailboxes = ref<MailMailbox[]>([])
|
||||
const search = ref('')
|
||||
let authenticationGeneration = 0
|
||||
let folderRequestGeneration = 0
|
||||
@@ -41,6 +52,8 @@ export const useMailStore = defineStore('mail', () => {
|
||||
folderRequestGeneration += 1
|
||||
accountEmail.value = ''
|
||||
counts.value = emptyCounts()
|
||||
listFilters.value = emptyListFilters()
|
||||
mailboxes.value = []
|
||||
items.value = []
|
||||
hasMore.value = false
|
||||
folder.value = 'inbox'
|
||||
@@ -57,7 +70,7 @@ export const useMailStore = defineStore('mail', () => {
|
||||
sessionGeneration += 1
|
||||
folderRequestGeneration += 1
|
||||
accountEmail.value = email
|
||||
await refreshCounts()
|
||||
await Promise.all([refreshCounts(), loadMailboxes()])
|
||||
}
|
||||
|
||||
async function login(email: string, password: string) {
|
||||
@@ -106,7 +119,7 @@ export const useMailStore = defineStore('mail', () => {
|
||||
}
|
||||
|
||||
async function loadFolder(
|
||||
nextFolder: MailFolder,
|
||||
nextFolder: MailFolderKey,
|
||||
nextSearch = '',
|
||||
append = false,
|
||||
): Promise<boolean> {
|
||||
@@ -114,10 +127,16 @@ export const useMailStore = defineStore('mail', () => {
|
||||
const session = sessionGeneration
|
||||
loading.value = true
|
||||
const offset = append ? items.value.length : 0
|
||||
const hasFilters =
|
||||
listFilters.value.address !== 'all' ||
|
||||
listFilters.value.direction !== 'all' ||
|
||||
listFilters.value.read !== 'all' ||
|
||||
listFilters.value.today
|
||||
const response = await nuiCall<MailListResponse>('mail:list', {
|
||||
folder: nextFolder,
|
||||
offset,
|
||||
search: nextSearch,
|
||||
...(hasFilters ? { filters: { ...listFilters.value } } : {}),
|
||||
})
|
||||
if (generation === folderRequestGeneration) loading.value = false
|
||||
if (
|
||||
@@ -151,6 +170,60 @@ export const useMailStore = defineStore('mail', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMailboxes(): Promise<boolean> {
|
||||
const email = accountEmail.value
|
||||
const session = sessionGeneration
|
||||
const response = await nuiCall<MailMailboxesResponse>('mail:mailboxes')
|
||||
if (
|
||||
session !== sessionGeneration ||
|
||||
email !== accountEmail.value ||
|
||||
!response.success ||
|
||||
!response.data
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
mailboxes.value = response.data.mailboxes
|
||||
return true
|
||||
}
|
||||
|
||||
async function createMailbox(name: string) {
|
||||
const session = sessionGeneration
|
||||
const response = await nuiCall<MailMailbox>('mail:create-mailbox', { name })
|
||||
if (response.success && session === sessionGeneration) {
|
||||
await loadMailboxes()
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
async function deleteMailbox(id: number): Promise<boolean> {
|
||||
const session = sessionGeneration
|
||||
const response = await nuiCall('mail:delete-mailbox', { id })
|
||||
if (response.success && session === sessionGeneration) {
|
||||
await Promise.all([refreshCounts(), loadMailboxes()])
|
||||
}
|
||||
return response.success
|
||||
}
|
||||
|
||||
async function moveEntry(
|
||||
id: number,
|
||||
mailboxId: number | null,
|
||||
): Promise<boolean> {
|
||||
const session = sessionGeneration
|
||||
const response = await nuiCall('mail:move', {
|
||||
id,
|
||||
mailboxId: mailboxId ?? 0,
|
||||
})
|
||||
if (response.success && session === sessionGeneration) {
|
||||
await Promise.all([
|
||||
refreshCounts(),
|
||||
loadMailboxes(),
|
||||
loadFolder(folder.value, search.value),
|
||||
])
|
||||
}
|
||||
return response.success
|
||||
}
|
||||
|
||||
async function openMessage(id: number): Promise<MailMessage | null> {
|
||||
const response = await nuiCall<MailMessage>('mail:get', { id })
|
||||
if (!response.success || !response.data) return null
|
||||
@@ -236,16 +309,22 @@ export const useMailStore = defineStore('mail', () => {
|
||||
accountEmail,
|
||||
bootstrap,
|
||||
counts,
|
||||
createMailbox,
|
||||
deleteDraft,
|
||||
deleteMailbox,
|
||||
deleteMany,
|
||||
emptyTrash,
|
||||
folder,
|
||||
hasMore,
|
||||
items,
|
||||
loadFolder,
|
||||
loadMailboxes,
|
||||
listFilters,
|
||||
loading,
|
||||
login,
|
||||
logout,
|
||||
mailboxes,
|
||||
moveEntry,
|
||||
mutateEntry,
|
||||
openDraft,
|
||||
openMessage,
|
||||
@@ -254,6 +333,9 @@ export const useMailStore = defineStore('mail', () => {
|
||||
saveDraft,
|
||||
search,
|
||||
send,
|
||||
setListFilters(nextFilters: MailListFilters) {
|
||||
listFilters.value = { ...nextFilters }
|
||||
},
|
||||
setCounts(nextCounts: MailCounts) {
|
||||
counts.value = nextCounts
|
||||
},
|
||||
|
||||
@@ -29,4 +29,30 @@ describe('phone locale fallback', () => {
|
||||
'Remove from favorites',
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps mailbox and filter copy translated with a partial server locale', () => {
|
||||
const phone = usePhoneStore()
|
||||
phone.open({ locales: { Apps: { mail: { name: 'Mail' } } } })
|
||||
|
||||
expect(phone.t('Apps.mail.newMailbox')).toBe('New Mailbox')
|
||||
expect(phone.t('Apps.mail.mailboxNamePlaceholder')).toBe('Mailbox name')
|
||||
expect(phone.t('Apps.mail.filterTitle')).toBe('Filter')
|
||||
expect(phone.t('Apps.mail.filterUnreadLabel')).toBe('Unread')
|
||||
expect(phone.t('Apps.mail.errors.mailbox_exists')).toBe(
|
||||
'A mailbox with that name already exists.',
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps provider capability copy translated with a partial server locale', () => {
|
||||
const phone = usePhoneStore()
|
||||
phone.open({ locales: { Apps: { radio: { name: 'Radio' } } } })
|
||||
|
||||
expect(phone.t('Apps.radio.notSupported')).toBe('Not supported')
|
||||
expect(phone.t('Apps.radio.providerFeatureUnavailable')).toBe(
|
||||
'Not supported by this voice service',
|
||||
)
|
||||
expect(phone.t('Apps.radio.displayNamePlaceholder')).toBe(
|
||||
'Character name',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2162,6 +2162,8 @@ const defaultLocales: LocaleTree = {
|
||||
disconnect: 'Disconnect',
|
||||
speaker: 'Speaker',
|
||||
speakerDescription: 'Play radio traffic through speaker mode',
|
||||
providerFeatureUnavailable: 'Not supported by this voice service',
|
||||
notSupported: 'Not supported',
|
||||
members: 'Currently connected ({count})',
|
||||
noMembers: 'No participants',
|
||||
history: 'Recently connected',
|
||||
@@ -2170,9 +2172,9 @@ const defaultLocales: LocaleTree = {
|
||||
badgePlaceholder: 'e.g. 231',
|
||||
profileSaved: 'Radio profile saved',
|
||||
displayName: 'Radio display name',
|
||||
displayNamePlaceholder: 'Leave empty to use your character name',
|
||||
displayNamePlaceholder: 'Character name',
|
||||
displayNameDescription:
|
||||
'This name is shown to other participants and in the built-in radio overlay.',
|
||||
'Leave empty to use your character name. This name is shown to other participants and in the built-in radio overlay.',
|
||||
displayNameNotAllowed:
|
||||
'Your current job or grade is not allowed to change the radio display name.',
|
||||
otherSettings: 'Other',
|
||||
@@ -3618,6 +3620,17 @@ const defaultLocales: LocaleTree = {
|
||||
passwordWarning:
|
||||
'Use an in-character password. Do not reuse a real-world password.',
|
||||
mailboxes: 'Mailboxes',
|
||||
editMailboxes: 'Edit',
|
||||
newMailbox: 'New Mailbox',
|
||||
mailboxName: 'Name',
|
||||
mailboxNamePlaceholder: 'Mailbox name',
|
||||
mailboxLocation: 'Mailbox Location',
|
||||
deleteMailbox: 'Delete {mailbox}',
|
||||
deleteMailboxTitle: 'Delete Mailbox?',
|
||||
deleteMailboxBody:
|
||||
'Messages in {mailbox} will return to their original mailbox.',
|
||||
moveToMailbox: 'Move to Mailbox',
|
||||
movedToMailbox: 'Moved to {mailbox}.',
|
||||
inbox: 'Inbox',
|
||||
sent: 'Sent',
|
||||
drafts: 'Drafts',
|
||||
@@ -3632,6 +3645,22 @@ const defaultLocales: LocaleTree = {
|
||||
messagePlaceholder: 'Write a message...',
|
||||
search: 'Search mail',
|
||||
messages: 'messages',
|
||||
toolbar: 'Mail tools',
|
||||
filterTitle: 'Filter',
|
||||
filteredBy: 'Filtered by',
|
||||
filterActiveLabel: 'Filtered by {filter}',
|
||||
filterStatus: 'Status',
|
||||
filterAddressed: 'Addressed',
|
||||
filterDirection: 'Direction',
|
||||
filterOther: 'Other',
|
||||
filterUnreadLabel: 'Unread',
|
||||
filterReadLabel: 'Read',
|
||||
filterReceived: 'Received',
|
||||
filterSent: 'Sent',
|
||||
filterToMe: 'To Me',
|
||||
filterFromMe: 'From Me',
|
||||
filterToday: 'Today',
|
||||
filterMultiple: '{count} Filters',
|
||||
select: 'Select',
|
||||
selectedCount: '{count} Selected',
|
||||
deleteSelected: 'Delete Selected',
|
||||
@@ -3681,6 +3710,12 @@ const defaultLocales: LocaleTree = {
|
||||
not_authenticated: 'Your mail session has ended. Sign in again.',
|
||||
request_failed: 'Mail is temporarily unavailable.',
|
||||
invalid_request: 'The mail request was invalid.',
|
||||
invalid_mailbox: 'Enter a mailbox name with up to 50 characters.',
|
||||
mailbox_exists: 'A mailbox with that name already exists.',
|
||||
mailbox_limit: 'You have reached the mailbox limit.',
|
||||
mailbox_not_found: 'That mailbox is no longer available.',
|
||||
message_not_found: 'That message is no longer available.',
|
||||
invalid_filter: 'That mail filter is not available.',
|
||||
default: 'The mail request failed.',
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user