mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
feat(messages): add draggable contact share sheet
This commit is contained in:
@@ -6409,6 +6409,62 @@ button {
|
||||
backdrop-filter: blur(32px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(180%);
|
||||
}
|
||||
.messages-media-picker-sheet {
|
||||
z-index: 90;
|
||||
--sky-overlay-backdrop: rgb(0 0 0 / 34%);
|
||||
--sky-sheet-background: rgb(247 247 251 / 96%);
|
||||
}
|
||||
.messages-media-picker-sheet .sky-sheet__panel {
|
||||
height: min(62%, 480px);
|
||||
max-height: 78%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgb(255 255 255 / 76%);
|
||||
border-bottom: 0;
|
||||
border-radius: 30px 30px 0 0;
|
||||
background: var(--sky-sheet-background);
|
||||
box-shadow: 0 -14px 38px rgb(20 26 43 / 18%);
|
||||
backdrop-filter: blur(34px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(34px) saturate(180%);
|
||||
}
|
||||
.messages-media-picker-sheet .sky-sheet__grabber {
|
||||
flex: none;
|
||||
height: 27px;
|
||||
}
|
||||
.messages-media-picker-sheet .messages-media-picker {
|
||||
position: relative;
|
||||
z-index: auto;
|
||||
inset: auto;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
.messages-media-picker-sheet .messages-media-picker > header {
|
||||
height: 44px;
|
||||
padding: 0 18px 0 20px;
|
||||
border-color: rgb(60 60 67 / 14%);
|
||||
}
|
||||
.messages-media-picker-sheet .messages-media-picker > header strong {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.messages-media-picker-sheet .messages-media-picker__contacts,
|
||||
.messages-media-picker-sheet .messages-media-picker__gifs {
|
||||
height: calc(100% - 44px);
|
||||
padding-bottom: calc(var(--sky-safe-area-bottom) + 10px);
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.messages-media-picker-sheet .messages-media-picker__contacts {
|
||||
padding-top: 6px;
|
||||
}
|
||||
.messages-media-picker__photos,
|
||||
.messages-media-picker__gifs,
|
||||
.messages-media-picker__videos {
|
||||
@@ -6768,6 +6824,14 @@ button {
|
||||
box-shadow: 0 -10px 34px rgb(0 0 0 / 38%);
|
||||
color: #f5f5f7;
|
||||
}
|
||||
.phone-app.dark .messages-media-picker-sheet {
|
||||
--sky-overlay-backdrop: rgb(0 0 0 / 52%);
|
||||
--sky-sheet-background: rgb(28 28 30 / 96%);
|
||||
}
|
||||
.phone-app.dark .messages-media-picker-sheet .sky-sheet__panel {
|
||||
border-color: rgb(255 255 255 / 11%);
|
||||
box-shadow: 0 -16px 42px rgb(0 0 0 / 46%);
|
||||
}
|
||||
.phone-app.dark .messages-gif-search,
|
||||
.phone-app.dark .messages-full-emoji-picker__search,
|
||||
.phone-app.dark .messages-full-emoji-picker > header button,
|
||||
|
||||
@@ -6,6 +6,10 @@ const source = readFileSync(
|
||||
new URL('./MessagesApp.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const styles = readFileSync(
|
||||
new URL('../../assets/main.css', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('MessagesApp Sky UI contract', () => {
|
||||
it('uses first-party Sky UI without direct Konsta markup', () => {
|
||||
@@ -22,6 +26,7 @@ describe('MessagesApp Sky UI contract', () => {
|
||||
expect(source).toContain('<SkyPillNavigation')
|
||||
expect(source).toContain('<SkySettingsGroup')
|
||||
expect(source).toContain('<SkySettingsRow')
|
||||
expect(source).toContain('<SkySheet')
|
||||
expect(source).toContain('<SkyToolbar')
|
||||
expect(source).not.toContain('<SkySegmented')
|
||||
})
|
||||
@@ -183,6 +188,30 @@ describe('MessagesApp Sky UI contract', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('opens contact sharing in a draggable Sky UI bottom sheet', () => {
|
||||
const sheetStart = source.indexOf('class="messages-media-picker-sheet"')
|
||||
const sheetEnd = source.indexOf('</SkySheet>', sheetStart)
|
||||
const sheet = source.slice(sheetStart, sheetEnd)
|
||||
|
||||
expect(sheetStart).toBeGreaterThan(-1)
|
||||
expect(sheetEnd).toBeGreaterThan(sheetStart)
|
||||
expect(sheet).toContain(
|
||||
':opened="activeCanMessage && attachmentPicker !== null"',
|
||||
)
|
||||
expect(sheet).toContain('swipe-to-close')
|
||||
expect(sheet).toContain('grabber-clickable')
|
||||
expect(sheet).toContain('@backdropclick="closeAttachmentPicker"')
|
||||
expect(sheet).toContain('@escape="closeAttachmentPicker"')
|
||||
expect(sheet).toContain('@grabberclick="closeAttachmentPicker"')
|
||||
expect(sheet).toContain('@swipeclose="closeAttachmentPicker"')
|
||||
expect(sheet).toContain('v-if="attachmentPicker === \'contacts\'"')
|
||||
expect(sheet).toContain('@click="sendContact(contact)"')
|
||||
expect(source).toContain('function closeAttachmentPicker(): void')
|
||||
expect(styles).toMatch(
|
||||
/\.messages-media-picker-sheet \.sky-sheet__panel\s*\{[^}]*border-radius:\s*30px 30px 0 0/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('uses a surface back target in the thread and a compact recipient row', () => {
|
||||
const threadStart = source.indexOf(
|
||||
'class="messages-sky-page messages-sky-thread"',
|
||||
@@ -278,10 +307,7 @@ describe('MessagesApp Sky UI contract', () => {
|
||||
)
|
||||
const contactProfile = source.slice(contactProfileStart, threadScrollStart)
|
||||
const blockDialogOpened = source.indexOf(':opened="blockDialogOpened"')
|
||||
const blockDialogStart = source.lastIndexOf(
|
||||
'<SkyDialog',
|
||||
blockDialogOpened,
|
||||
)
|
||||
const blockDialogStart = source.lastIndexOf('<SkyDialog', blockDialogOpened)
|
||||
const toastStart = source.indexOf('<SkyToast', blockDialogStart)
|
||||
const blockDialog = source.slice(blockDialogStart, toastStart)
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
SkySearchbar,
|
||||
SkySettingsGroup,
|
||||
SkySettingsRow,
|
||||
SkySheet,
|
||||
SkySpinner,
|
||||
SkyToast,
|
||||
SkyToolbar,
|
||||
@@ -618,6 +619,10 @@ function openContactPicker(): void {
|
||||
emojiOpen.value = false
|
||||
}
|
||||
|
||||
function closeAttachmentPicker(): void {
|
||||
attachmentPicker.value = null
|
||||
}
|
||||
|
||||
function openEmojiPicker(): void {
|
||||
attachmentMenuOpen.value = false
|
||||
attachmentPicker.value = null
|
||||
@@ -1639,103 +1644,121 @@ onBeforeUnmount(() => {
|
||||
</SkyGlass>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="activeCanMessage && attachmentPicker"
|
||||
class="messages-media-picker"
|
||||
:aria-hidden="contactDetailsOpen"
|
||||
:inert="contactDetailsOpen || undefined"
|
||||
<SkySheet
|
||||
class="messages-media-picker-sheet"
|
||||
:opened="activeCanMessage && attachmentPicker !== null"
|
||||
:aria-label="
|
||||
phone.t(
|
||||
attachmentPicker === 'contacts'
|
||||
? 'Apps.messages.contacts'
|
||||
: 'Apps.messages.gifs',
|
||||
)
|
||||
"
|
||||
swipe-to-close
|
||||
grabber-clickable
|
||||
:grabber-label="phone.t('Common.close')"
|
||||
@backdropclick="closeAttachmentPicker"
|
||||
@escape="closeAttachmentPicker"
|
||||
@grabberclick="closeAttachmentPicker"
|
||||
@swipeclose="closeAttachmentPicker"
|
||||
>
|
||||
<header>
|
||||
<strong>
|
||||
{{
|
||||
phone.t(
|
||||
attachmentPicker === 'contacts'
|
||||
? 'Apps.messages.contacts'
|
||||
: 'Apps.messages.gifs',
|
||||
)
|
||||
}}
|
||||
</strong>
|
||||
<SkyLink @click="attachmentPicker = null">
|
||||
{{ phone.t('Common.done') }}
|
||||
</SkyLink>
|
||||
</header>
|
||||
<SkyList
|
||||
v-if="attachmentPicker === 'contacts'"
|
||||
inset
|
||||
strong
|
||||
class="messages-media-picker__contacts"
|
||||
<section
|
||||
class="messages-media-picker"
|
||||
:aria-hidden="contactDetailsOpen"
|
||||
:inert="contactDetailsOpen || undefined"
|
||||
>
|
||||
<SkyListItem
|
||||
v-for="contact in calls.contacts"
|
||||
:key="contact.id"
|
||||
link
|
||||
link-component="button"
|
||||
:title="contact.name"
|
||||
:subtitle="contact.organization || contact.phone_number"
|
||||
@click="sendContact(contact)"
|
||||
<header>
|
||||
<strong>
|
||||
{{
|
||||
phone.t(
|
||||
attachmentPicker === 'contacts'
|
||||
? 'Apps.messages.contacts'
|
||||
: 'Apps.messages.gifs',
|
||||
)
|
||||
}}
|
||||
</strong>
|
||||
<SkyLink @click="closeAttachmentPicker">
|
||||
{{ phone.t('Common.done') }}
|
||||
</SkyLink>
|
||||
</header>
|
||||
<SkyList
|
||||
v-if="attachmentPicker === 'contacts'"
|
||||
inset
|
||||
strong
|
||||
class="messages-media-picker__contacts"
|
||||
>
|
||||
<template #media>
|
||||
<span class="messages-avatar messages-avatar--small">
|
||||
<img
|
||||
v-if="contact.avatar_url"
|
||||
class="messages-avatar__image"
|
||||
:src="contact.avatar_url"
|
||||
alt=""
|
||||
/>
|
||||
<span v-else class="messages-avatar__initials">{{
|
||||
contactInitials(contact.phone_number)
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<p v-if="!calls.contacts.length" class="messages-media-picker__empty">
|
||||
{{ phone.t('Apps.messages.noContactsToShare') }}
|
||||
</p>
|
||||
</SkyList>
|
||||
<div v-else class="messages-media-picker__gifs">
|
||||
<SkySearchbar
|
||||
v-model="gifQuery"
|
||||
class="messages-gif-search"
|
||||
:label="phone.t('Apps.messages.searchGifs')"
|
||||
:placeholder="phone.t('Apps.messages.searchGifs')"
|
||||
:clear-label="phone.t('Common.clear')"
|
||||
@input="queueGifSearch"
|
||||
@clear="queueGifSearch"
|
||||
/>
|
||||
<button
|
||||
v-for="gif in gifResults"
|
||||
:key="gif.id"
|
||||
type="button"
|
||||
:aria-label="gif.title"
|
||||
:style="{
|
||||
aspectRatio: `${Math.max(1, gif.width)} / ${Math.max(1, gif.height)}`,
|
||||
}"
|
||||
@click="sendAttachment('gif', gif.url)"
|
||||
>
|
||||
<img :src="gif.previewUrl" :alt="gif.title" loading="lazy" />
|
||||
</button>
|
||||
<button
|
||||
v-if="gifResults.length && gifHasMore && !gifLoading"
|
||||
type="button"
|
||||
class="messages-gif-more"
|
||||
@click="loadGifs()"
|
||||
>
|
||||
{{ phone.t('Apps.messages.loadMore') }}
|
||||
</button>
|
||||
<div v-if="gifError && !gifLoading" class="messages-gif-error">
|
||||
<ImagePlay :size="24" />
|
||||
<strong>{{ errorText(gifError) }}</strong>
|
||||
<button type="button" @click="loadGifs(true)">
|
||||
{{ phone.t('Apps.messages.retryGifs') }}
|
||||
<SkyListItem
|
||||
v-for="contact in calls.contacts"
|
||||
:key="contact.id"
|
||||
link
|
||||
link-component="button"
|
||||
:title="contact.name"
|
||||
:subtitle="contact.organization || contact.phone_number"
|
||||
@click="sendContact(contact)"
|
||||
>
|
||||
<template #media>
|
||||
<span class="messages-avatar messages-avatar--small">
|
||||
<img
|
||||
v-if="contact.avatar_url"
|
||||
class="messages-avatar__image"
|
||||
:src="contact.avatar_url"
|
||||
alt=""
|
||||
/>
|
||||
<span v-else class="messages-avatar__initials">{{
|
||||
contactInitials(contact.phone_number)
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<p v-if="!calls.contacts.length" class="messages-media-picker__empty">
|
||||
{{ phone.t('Apps.messages.noContactsToShare') }}
|
||||
</p>
|
||||
</SkyList>
|
||||
<div v-else class="messages-media-picker__gifs">
|
||||
<SkySearchbar
|
||||
v-model="gifQuery"
|
||||
class="messages-gif-search"
|
||||
:label="phone.t('Apps.messages.searchGifs')"
|
||||
:placeholder="phone.t('Apps.messages.searchGifs')"
|
||||
:clear-label="phone.t('Common.clear')"
|
||||
@input="queueGifSearch"
|
||||
@clear="queueGifSearch"
|
||||
/>
|
||||
<button
|
||||
v-for="gif in gifResults"
|
||||
:key="gif.id"
|
||||
type="button"
|
||||
:aria-label="gif.title"
|
||||
:style="{
|
||||
aspectRatio: `${Math.max(1, gif.width)} / ${Math.max(1, gif.height)}`,
|
||||
}"
|
||||
@click="sendAttachment('gif', gif.url)"
|
||||
>
|
||||
<img :src="gif.previewUrl" :alt="gif.title" loading="lazy" />
|
||||
</button>
|
||||
<button
|
||||
v-if="gifResults.length && gifHasMore && !gifLoading"
|
||||
type="button"
|
||||
class="messages-gif-more"
|
||||
@click="loadGifs()"
|
||||
>
|
||||
{{ phone.t('Apps.messages.loadMore') }}
|
||||
</button>
|
||||
<div v-if="gifError && !gifLoading" class="messages-gif-error">
|
||||
<ImagePlay :size="24" />
|
||||
<strong>{{ errorText(gifError) }}</strong>
|
||||
<button type="button" @click="loadGifs(true)">
|
||||
{{ phone.t('Apps.messages.retryGifs') }}
|
||||
</button>
|
||||
</div>
|
||||
<SkySpinner
|
||||
v-if="gifLoading"
|
||||
class="messages-gif-loading"
|
||||
:label="phone.t('Common.loading')"
|
||||
/>
|
||||
</div>
|
||||
<SkySpinner
|
||||
v-if="gifLoading"
|
||||
class="messages-gif-loading"
|
||||
:label="phone.t('Common.loading')"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</SkySheet>
|
||||
|
||||
<FullEmojiPicker
|
||||
v-if="activeCanMessage && emojiOpen"
|
||||
|
||||
Reference in New Issue
Block a user