mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
3850 lines
99 KiB
Vue
3850 lines
99 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
SkyBlock,
|
|
SkyButton,
|
|
SkyDialog,
|
|
SkyDialogButton,
|
|
SkyGlass,
|
|
SkyList,
|
|
SkyField,
|
|
SkyAppPage,
|
|
SkySheet,
|
|
SkyTabBar,
|
|
SkyTabButton,
|
|
} from '@/ui'
|
|
import {
|
|
Camera,
|
|
Check,
|
|
ChevronRight,
|
|
ChevronLeft,
|
|
Clock3,
|
|
ContactRound,
|
|
Delete,
|
|
Info,
|
|
Images,
|
|
Grid3X3,
|
|
Mail,
|
|
MessageCircle,
|
|
MicOff,
|
|
MoreHorizontal,
|
|
Phone,
|
|
PhoneIncoming,
|
|
PhoneOff,
|
|
PhoneOutgoing,
|
|
Plus,
|
|
Search,
|
|
Smartphone,
|
|
Star,
|
|
UserRound,
|
|
Video,
|
|
Volume2,
|
|
X,
|
|
} from 'lucide-vue-next'
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
import { useCallsStore } from '@/stores/calls'
|
|
import { useEasyShareStore } from '@/stores/easyshare'
|
|
import { useMessageMediaStore } from '@/stores/messageMedia'
|
|
import { useMessagesStore } from '@/stores/messages'
|
|
import { usePhoneStore } from '@/stores/phone'
|
|
import type { EasySharePayload } from '@/types/easyshare'
|
|
import type { PhoneContact, RecentCall } from '@/types/phone'
|
|
import { nuiCall } from '@/utils/nui'
|
|
import { formatPhoneNumber, normalizePhoneNumber } from '@/utils/phone'
|
|
|
|
type PhoneTab = 'recents' | 'contacts' | 'keypad'
|
|
type RecentFilter = 'all' | 'missed'
|
|
type ContactProfileAction = 'message' | 'call' | 'video' | 'mail'
|
|
type ContactPhotoContext = {
|
|
avatarMediaId: number | null
|
|
avatarUrl: string
|
|
contactId?: string
|
|
email: string
|
|
firstName: string
|
|
lastName: string
|
|
notes: string
|
|
organization: string
|
|
phoneNumber: string
|
|
}
|
|
|
|
const phone = usePhoneStore()
|
|
const calls = useCallsStore()
|
|
const easyShare = useEasyShareStore()
|
|
const mediaPicker = useMessageMediaStore()
|
|
const messages = useMessagesStore()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const tab = ref<PhoneTab>('recents')
|
|
const query = ref('')
|
|
const recentQuery = ref('')
|
|
const recentFilter = ref<RecentFilter>('all')
|
|
const selectedNumber = ref('')
|
|
const viewingOwnCard = ref(false)
|
|
const phoneContent = ref<HTMLElement | null>(null)
|
|
const keypad = ref('')
|
|
const editorOpened = ref(false)
|
|
const editingContact = ref<PhoneContact | null>(null)
|
|
const contactFirstName = ref('')
|
|
const contactLastName = ref('')
|
|
const contactOrganization = ref('')
|
|
const contactEmail = ref('')
|
|
const contactNotes = ref('')
|
|
const contactNumber = ref('')
|
|
const contactAvatarMediaId = ref<number | null>(null)
|
|
const contactAvatarUrl = ref('')
|
|
const error = ref('')
|
|
const callMoreOpened = ref(false)
|
|
const callKeypadOpened = ref(false)
|
|
const inCallKeypad = ref('')
|
|
const blockDialogOpened = ref(false)
|
|
const blockTargetNumber = ref('')
|
|
const callSpeakerPending = ref(false)
|
|
const callMutePending = ref(false)
|
|
const callElapsedSeconds = ref(0)
|
|
let callClock: number | null = null
|
|
const tabs = [
|
|
{ id: 'recents', icon: Clock3 },
|
|
{ id: 'contacts', icon: ContactRound },
|
|
{ id: 'keypad', icon: Phone },
|
|
] as const
|
|
const keypadKeys = [
|
|
{ digit: '1', letters: '' },
|
|
{ digit: '2', letters: 'ABC' },
|
|
{ digit: '3', letters: 'DEF' },
|
|
{ digit: '4', letters: 'GHI' },
|
|
{ digit: '5', letters: 'JKL' },
|
|
{ digit: '6', letters: 'MNO' },
|
|
{ digit: '7', letters: 'PQRS' },
|
|
{ digit: '8', letters: 'TUV' },
|
|
{ digit: '9', letters: 'WXYZ' },
|
|
{ digit: '*', letters: '' },
|
|
{ digit: '0', letters: '+' },
|
|
{ digit: '#', letters: '' },
|
|
] as const
|
|
const contactProfileActions = [
|
|
{ id: 'message', icon: MessageCircle },
|
|
{ id: 'call', icon: Phone },
|
|
{ id: 'video', icon: Video },
|
|
{ id: 'mail', icon: Mail },
|
|
] as const
|
|
const contactAlphabet = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', '#']
|
|
const visibleContacts = computed(() => {
|
|
const needle = query.value.trim().toLowerCase()
|
|
if (!needle) return calls.contacts
|
|
return calls.contacts.filter(
|
|
(contact) =>
|
|
contact.name.toLowerCase().includes(needle) ||
|
|
contact.phone_number.includes(needle.replace(/\D/g, '')),
|
|
)
|
|
})
|
|
const contactsByNumber = computed(
|
|
() =>
|
|
new Map(calls.contacts.map((contact) => [contact.phone_number, contact])),
|
|
)
|
|
const groupedContacts = computed(() => {
|
|
const groups = new Map<string, PhoneContact[]>()
|
|
const sorted = [...visibleContacts.value].sort((left, right) =>
|
|
left.name.localeCompare(right.name, phone.lang, { sensitivity: 'base' }),
|
|
)
|
|
for (const contact of sorted) {
|
|
const firstCharacter = contact.name.trim().charAt(0).toUpperCase()
|
|
const letter = /^[A-Z]$/.test(firstCharacter) ? firstCharacter : '#'
|
|
const group = groups.get(letter) ?? []
|
|
group.push(contact)
|
|
groups.set(letter, group)
|
|
}
|
|
return contactAlphabet
|
|
.filter((letter) => groups.has(letter))
|
|
.map((letter) => ({ contacts: groups.get(letter) ?? [], letter }))
|
|
})
|
|
const favoriteContacts = computed(() =>
|
|
calls.contacts
|
|
.filter((contact) => Boolean(contact.favorite))
|
|
.sort((left, right) =>
|
|
left.name.localeCompare(right.name, phone.lang, { sensitivity: 'base' }),
|
|
),
|
|
)
|
|
const visibleRecents = computed(() => {
|
|
const needle = recentQuery.value.trim().toLowerCase()
|
|
return calls.recents.filter((recent) => {
|
|
const contact = calls.contacts.find(
|
|
(entry) => entry.phone_number === recent.other_number,
|
|
)
|
|
const matchesFilter =
|
|
recentFilter.value === 'all' ||
|
|
['missed', 'no_answer'].includes(recent.status)
|
|
const matchesQuery =
|
|
!needle ||
|
|
recent.other_number.includes(needle.replace(/\D/g, '')) ||
|
|
contact?.name.toLowerCase().includes(needle)
|
|
return matchesFilter && matchesQuery
|
|
})
|
|
})
|
|
const selectedContact = computed(
|
|
() =>
|
|
calls.contacts.find(
|
|
(contact) => contact.phone_number === selectedNumber.value,
|
|
) ?? null,
|
|
)
|
|
const selectedHistory = computed(() =>
|
|
calls.recents.filter(
|
|
(recent) => recent.other_number === selectedNumber.value,
|
|
),
|
|
)
|
|
const selectedDisplayName = computed(() => {
|
|
if (viewingOwnCard.value) return phone.t('Apps.phone.myCard')
|
|
return selectedContact.value?.name ?? formatPhoneNumber(selectedNumber.value)
|
|
})
|
|
const keypadDisplay = computed(() => {
|
|
if (!keypad.value) return ''
|
|
return /[*#]/.test(keypad.value)
|
|
? keypad.value
|
|
: formatPhoneNumber(keypad.value)
|
|
})
|
|
const keypadSuggestions = computed(() => {
|
|
const enteredDigits = keypad.value.replace(/\D/g, '')
|
|
if (enteredDigits.length < 2 || /[*#]/.test(keypad.value)) return []
|
|
return calls.contacts
|
|
.filter((contact) =>
|
|
contact.phone_number.replace(/\D/g, '').startsWith(enteredDigits),
|
|
)
|
|
.sort((left, right) => {
|
|
const leftExact = left.phone_number.replace(/\D/g, '') === enteredDigits
|
|
const rightExact = right.phone_number.replace(/\D/g, '') === enteredDigits
|
|
if (leftExact !== rightExact) return leftExact ? -1 : 1
|
|
return left.name.localeCompare(right.name, phone.lang, {
|
|
sensitivity: 'base',
|
|
})
|
|
})
|
|
})
|
|
const activeCallLabel = computed(() => {
|
|
const call = calls.activeCall
|
|
if (!call) return ''
|
|
if (call.state === 'ringing') {
|
|
return phone.t(
|
|
call.direction === 'incoming'
|
|
? 'Apps.phone.incoming'
|
|
: 'Apps.phone.calling',
|
|
)
|
|
}
|
|
const key = call.state === 'no_answer' ? 'noAnswer' : call.state
|
|
return phone.t(`Apps.phone.${key}`)
|
|
})
|
|
const activeCallStatus = computed(() => {
|
|
if (calls.activeCall?.state !== 'connected') return activeCallLabel.value
|
|
const minutes = Math.floor(callElapsedSeconds.value / 60)
|
|
const seconds = String(callElapsedSeconds.value % 60).padStart(2, '0')
|
|
return `${String(minutes).padStart(2, '0')}:${seconds}`
|
|
})
|
|
const activeCallContact = computed(
|
|
() =>
|
|
calls.contacts.find(
|
|
(contact) => contact.phone_number === calls.activeCall?.otherNumber,
|
|
) ?? null,
|
|
)
|
|
const contactEditorInitials = computed(() =>
|
|
contactFirstName.value.trim().charAt(0).toUpperCase(),
|
|
)
|
|
|
|
function eventValue(event: Event): string {
|
|
return (event.target as HTMLInputElement).value
|
|
}
|
|
|
|
function contactNameFor(number: string): string {
|
|
return contactsByNumber.value.get(number)?.name ?? formatPhoneNumber(number)
|
|
}
|
|
|
|
function openContact(contact?: PhoneContact, number = ''): void {
|
|
editingContact.value = contact ?? null
|
|
const nameParts = contact?.name.trim().split(/\s+/) ?? []
|
|
contactFirstName.value = nameParts.shift() ?? ''
|
|
contactLastName.value = nameParts.join(' ')
|
|
contactOrganization.value = contact?.organization ?? ''
|
|
contactEmail.value = contact?.email ?? ''
|
|
contactNotes.value = contact?.notes ?? ''
|
|
contactNumber.value = contact?.phone_number ?? number
|
|
contactAvatarMediaId.value = contact?.avatar_media_id ?? null
|
|
contactAvatarUrl.value = contact?.avatar_url ?? ''
|
|
error.value = ''
|
|
editorOpened.value = true
|
|
}
|
|
|
|
function openContactPhotoPicker(source: 'camera' | 'photos'): void {
|
|
mediaPicker.begin('phone:contact-photo', 'photo', '/apps/phone', 1, {
|
|
avatarMediaId: contactAvatarMediaId.value,
|
|
avatarUrl: contactAvatarUrl.value,
|
|
contactId: editingContact.value?.id,
|
|
email: contactEmail.value,
|
|
firstName: contactFirstName.value,
|
|
lastName: contactLastName.value,
|
|
notes: contactNotes.value,
|
|
organization: contactOrganization.value,
|
|
phoneNumber: contactNumber.value,
|
|
} satisfies ContactPhotoContext)
|
|
editorOpened.value = false
|
|
void router.push({
|
|
path: `/apps/${source}`,
|
|
query: { mediaAttachment: 'photo' },
|
|
})
|
|
}
|
|
|
|
function removeContactPhoto(): void {
|
|
contactAvatarMediaId.value = null
|
|
contactAvatarUrl.value = ''
|
|
}
|
|
|
|
function openRecentDetail(number: string): void {
|
|
viewingOwnCard.value = false
|
|
selectedNumber.value = number
|
|
error.value = ''
|
|
void nextTick(() => phoneContent.value?.scrollTo({ top: 0 }))
|
|
}
|
|
|
|
function openMyCard(): void {
|
|
if (!phone.device?.sim) return
|
|
viewingOwnCard.value = true
|
|
selectedNumber.value = phone.device.sim.number
|
|
error.value = ''
|
|
void nextTick(() => phoneContent.value?.scrollTo({ top: 0 }))
|
|
}
|
|
|
|
function closeRecentDetail(): void {
|
|
viewingOwnCard.value = false
|
|
selectedNumber.value = ''
|
|
}
|
|
|
|
function selectTab(nextTab: PhoneTab): void {
|
|
viewingOwnCard.value = false
|
|
selectedNumber.value = ''
|
|
tab.value = nextTab
|
|
}
|
|
|
|
function scrollToContactGroup(letter: string): void {
|
|
const scroller = phoneContent.value
|
|
if (letter === 'favorites') {
|
|
const target = document.getElementById('phone-contact-group-favorites')
|
|
if (!scroller || !target) return
|
|
const scrollTop =
|
|
scroller.scrollTop +
|
|
target.getBoundingClientRect().top -
|
|
scroller.getBoundingClientRect().top -
|
|
8
|
|
scroller.scrollTo({ behavior: 'smooth', top: scrollTop })
|
|
return
|
|
}
|
|
const availableLetters = new Set(
|
|
groupedContacts.value.map((group) => group.letter),
|
|
)
|
|
let targetLetter = letter
|
|
if (!availableLetters.has(targetLetter)) {
|
|
const requestedIndex = contactAlphabet.indexOf(letter)
|
|
for (let distance = 1; distance < contactAlphabet.length; distance += 1) {
|
|
const nextLetter = contactAlphabet[requestedIndex + distance]
|
|
if (nextLetter && availableLetters.has(nextLetter)) {
|
|
targetLetter = nextLetter
|
|
break
|
|
}
|
|
const previousLetter = contactAlphabet[requestedIndex - distance]
|
|
if (previousLetter && availableLetters.has(previousLetter)) {
|
|
targetLetter = previousLetter
|
|
break
|
|
}
|
|
}
|
|
}
|
|
const suffix = targetLetter === '#' ? 'other' : targetLetter.toLowerCase()
|
|
const target = document.getElementById(`phone-contact-group-${suffix}`)
|
|
if (!scroller || !target) return
|
|
const scrollTop =
|
|
scroller.scrollTop +
|
|
target.getBoundingClientRect().top -
|
|
scroller.getBoundingClientRect().top -
|
|
8
|
|
scroller.scrollTo({ behavior: 'smooth', top: scrollTop })
|
|
}
|
|
|
|
async function toggleSelectedFavorite(): Promise<void> {
|
|
const contact = selectedContact.value
|
|
if (!contact) return
|
|
const response = await calls.setContactFavorite(
|
|
contact.id,
|
|
!Boolean(contact.favorite),
|
|
)
|
|
if (!response.success) {
|
|
error.value = phone.t('Apps.phone.errors.contact_favorite_failed')
|
|
}
|
|
}
|
|
|
|
async function saveContact(): Promise<void> {
|
|
if (editingContact.value?.readonly) {
|
|
error.value = phone.t('Apps.phone.errors.readonly_contact')
|
|
return
|
|
}
|
|
const number = normalizePhoneNumber(contactNumber.value)
|
|
const name = [contactFirstName.value, contactLastName.value]
|
|
.map((part) => part.trim())
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
if (!name || !number) {
|
|
error.value = phone.t('Apps.phone.errors.invalid_contact')
|
|
return
|
|
}
|
|
const response = await calls.saveContact({
|
|
avatarMediaId: contactAvatarMediaId.value,
|
|
email: contactEmail.value.trim(),
|
|
id: editingContact.value?.id,
|
|
name,
|
|
notes: contactNotes.value.trim(),
|
|
organization: contactOrganization.value.trim(),
|
|
phoneNumber: number,
|
|
})
|
|
if (!response.success) {
|
|
error.value = phone.t(`Apps.phone.errors.${response.error ?? 'default'}`)
|
|
return
|
|
}
|
|
editorOpened.value = false
|
|
viewingOwnCard.value = false
|
|
selectedNumber.value = number
|
|
void nextTick(() => phoneContent.value?.scrollTo({ top: 0 }))
|
|
}
|
|
|
|
async function openMessage(number: string): Promise<void> {
|
|
error.value = ''
|
|
if (await messages.openThread(number)) {
|
|
void router.push('/apps/messages')
|
|
return
|
|
}
|
|
error.value = phone.t('Apps.phone.errors.message_unavailable')
|
|
}
|
|
|
|
function triggerContactAction(action: ContactProfileAction): void {
|
|
if (action === 'call') {
|
|
if (selectedContact.value?.canCall === false) return
|
|
void startCall(selectedNumber.value)
|
|
return
|
|
}
|
|
if (action === 'message' && selectedContact.value?.canMessage !== false) {
|
|
void openMessage(selectedNumber.value)
|
|
}
|
|
}
|
|
|
|
async function startCall(number: string): Promise<void> {
|
|
error.value = ''
|
|
const response = await calls.dial(number)
|
|
if (!response.success) {
|
|
error.value = phone.t(`Apps.phone.errors.${response.error ?? 'default'}`)
|
|
}
|
|
}
|
|
|
|
async function answerCall(): Promise<void> {
|
|
error.value = ''
|
|
const response = await calls.answer()
|
|
if (!response.success) {
|
|
error.value = phone.t(`Apps.phone.errors.${response.error ?? 'default'}`)
|
|
}
|
|
}
|
|
|
|
async function toggleCallSpeaker(): Promise<void> {
|
|
const call = calls.activeCall
|
|
if (
|
|
!call ||
|
|
call.state !== 'connected' ||
|
|
!call.speakerSupported ||
|
|
callSpeakerPending.value
|
|
) {
|
|
return
|
|
}
|
|
|
|
error.value = ''
|
|
callSpeakerPending.value = true
|
|
const response = await calls.setSpeaker(!call.speakerEnabled)
|
|
callSpeakerPending.value = false
|
|
if (!response.success) {
|
|
error.value = phone.t(`Apps.phone.errors.${response.error ?? 'default'}`)
|
|
}
|
|
}
|
|
|
|
async function toggleCallMute(): Promise<void> {
|
|
const call = calls.activeCall
|
|
if (
|
|
!call ||
|
|
call.state !== 'connected' ||
|
|
!call.muteSupported ||
|
|
callMutePending.value
|
|
) {
|
|
return
|
|
}
|
|
|
|
error.value = ''
|
|
callMutePending.value = true
|
|
const response = await calls.setMuted(!call.muted)
|
|
callMutePending.value = false
|
|
if (!response.success) {
|
|
error.value = phone.t(`Apps.phone.errors.${response.error ?? 'default'}`)
|
|
}
|
|
}
|
|
|
|
function updateCallElapsed(): void {
|
|
const call = calls.activeCall
|
|
if (!call || call.state !== 'connected') {
|
|
callElapsedSeconds.value = 0
|
|
return
|
|
}
|
|
const timestamp = call.answeredAt ?? call.startedAt
|
|
const startedAt = timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp
|
|
callElapsedSeconds.value = Math.max(
|
|
0,
|
|
Math.floor((Date.now() - startedAt) / 1000),
|
|
)
|
|
}
|
|
|
|
function openCallContact(): void {
|
|
const call = calls.activeCall
|
|
if (!call) return
|
|
callMoreOpened.value = false
|
|
openContact(activeCallContact.value ?? undefined, call.otherNumber)
|
|
}
|
|
|
|
function shareSelectedContact(): void {
|
|
const contact = selectedContact.value
|
|
if (!contact) return
|
|
easyShare.open({
|
|
appId: 'phone',
|
|
copyText: `${contact.name}\n${contact.phone_number}`,
|
|
id: contact.id,
|
|
imageUrl: contact.avatar_url,
|
|
kind: 'contact',
|
|
subtitle: formatPhoneNumber(contact.phone_number),
|
|
title: contact.name,
|
|
})
|
|
}
|
|
|
|
async function shareOwnProfile(): Promise<void> {
|
|
const response = await nuiCall<EasySharePayload>('easyshare:own-contact')
|
|
if (!response.success || !response.data) {
|
|
error.value = phone.t(
|
|
`Apps.easyShare.errors.${response.error ?? 'request_failed'}`,
|
|
)
|
|
return
|
|
}
|
|
error.value = ''
|
|
easyShare.open(response.data)
|
|
}
|
|
|
|
function messageActiveCaller(): void {
|
|
const number = calls.activeCall?.otherNumber
|
|
callMoreOpened.value = false
|
|
if (number) void openMessage(number)
|
|
}
|
|
|
|
async function removeActiveCallerContact(): Promise<void> {
|
|
const contact = activeCallContact.value
|
|
if (!contact) return
|
|
if (await calls.deleteContact(contact.id)) {
|
|
callMoreOpened.value = false
|
|
return
|
|
}
|
|
error.value = phone.t('Apps.phone.errors.contact_remove_failed')
|
|
}
|
|
|
|
function confirmBlockNumber(number: string): void {
|
|
callMoreOpened.value = false
|
|
blockTargetNumber.value = number
|
|
blockDialogOpened.value = true
|
|
}
|
|
|
|
async function blockNumber(): Promise<void> {
|
|
const number = blockTargetNumber.value
|
|
blockDialogOpened.value = false
|
|
error.value = ''
|
|
const response = await calls.blockNumber(number)
|
|
if (!response.success) {
|
|
error.value = phone.t(`Apps.phone.errors.${response.error ?? 'default'}`)
|
|
}
|
|
}
|
|
|
|
function openCallKeypad(): void {
|
|
callMoreOpened.value = false
|
|
inCallKeypad.value = ''
|
|
callKeypadOpened.value = true
|
|
}
|
|
|
|
function addInCallDigit(digit: string): void {
|
|
if (inCallKeypad.value.length < 24) inCallKeypad.value += digit
|
|
}
|
|
|
|
async function deleteEditedContact(): Promise<void> {
|
|
if (!editingContact.value) return
|
|
if (editingContact.value.readonly) {
|
|
error.value = phone.t('Apps.phone.errors.readonly_contact')
|
|
return
|
|
}
|
|
if (await calls.deleteContact(editingContact.value.id)) {
|
|
editorOpened.value = false
|
|
}
|
|
}
|
|
|
|
function addDigit(digit: string): void {
|
|
if (keypad.value.length < 10) keypad.value += digit
|
|
}
|
|
|
|
function isEditableTarget(target: EventTarget | null): boolean {
|
|
if (!(target instanceof HTMLElement)) return false
|
|
return (
|
|
target.isContentEditable ||
|
|
target instanceof HTMLInputElement ||
|
|
target instanceof HTMLSelectElement ||
|
|
target instanceof HTMLTextAreaElement
|
|
)
|
|
}
|
|
|
|
function handleKeypadKeyboard(event: KeyboardEvent): void {
|
|
if (
|
|
tab.value !== 'keypad' ||
|
|
selectedNumber.value ||
|
|
event.defaultPrevented ||
|
|
event.isComposing ||
|
|
event.altKey ||
|
|
event.ctrlKey ||
|
|
event.metaKey ||
|
|
isEditableTarget(event.target)
|
|
) {
|
|
return
|
|
}
|
|
|
|
if (/^[0-9*#]$/.test(event.key)) {
|
|
event.preventDefault()
|
|
addDigit(event.key)
|
|
return
|
|
}
|
|
|
|
if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
event.preventDefault()
|
|
keypad.value = keypad.value.slice(0, -1)
|
|
return
|
|
}
|
|
|
|
if (event.key === 'Enter' && keypad.value && !event.repeat) {
|
|
event.preventDefault()
|
|
void startCall(keypad.value)
|
|
}
|
|
}
|
|
|
|
function chooseKeypadSuggestion(contact: PhoneContact): void {
|
|
keypad.value = contact.phone_number.replace(/\D/g, '').slice(0, 10)
|
|
}
|
|
|
|
function recentStatus(recent: RecentCall): string {
|
|
const key = recent.status === 'no_answer' ? 'noAnswer' : recent.status
|
|
return phone.t(`Apps.phone.${key}`)
|
|
}
|
|
|
|
function recentSubtitle(recent: RecentCall): string {
|
|
const status = recentStatus(recent)
|
|
const direction = phone.t(
|
|
`Apps.phone.${recent.direction === 'incoming' ? 'incomingDirection' : 'outgoingDirection'}`,
|
|
)
|
|
if (!recent.duration_seconds) return `${direction} · ${status}`
|
|
const minutes = Math.floor(recent.duration_seconds / 60)
|
|
const seconds = String(recent.duration_seconds % 60).padStart(2, '0')
|
|
return `${direction} · ${status} · ${minutes}:${seconds}`
|
|
}
|
|
|
|
function contactInitials(number: string): string {
|
|
const contact = calls.contacts.find((entry) => entry.phone_number === number)
|
|
if (!contact) return ''
|
|
return contact.name.trim().charAt(0).toUpperCase()
|
|
}
|
|
|
|
function formatRecentDate(value: string): string {
|
|
const date = new Date(value)
|
|
const now = new Date()
|
|
const sameDay =
|
|
date.getFullYear() === now.getFullYear() &&
|
|
date.getMonth() === now.getMonth() &&
|
|
date.getDate() === now.getDate()
|
|
if (sameDay) {
|
|
return new Intl.DateTimeFormat(phone.lang, {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(date)
|
|
}
|
|
const daysAgo = Math.floor(
|
|
(new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime() -
|
|
new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()) /
|
|
86_400_000,
|
|
)
|
|
if (daysAgo < 7) {
|
|
return new Intl.DateTimeFormat(phone.lang, { weekday: 'long' }).format(date)
|
|
}
|
|
return new Intl.DateTimeFormat(phone.lang, {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: '2-digit',
|
|
}).format(date)
|
|
}
|
|
|
|
onMounted(async () => {
|
|
window.addEventListener('keydown', handleKeypadKeyboard)
|
|
await calls.bootstrap()
|
|
const photoSelection = mediaPicker.consumeMany<ContactPhotoContext>(
|
|
'phone:contact-photo',
|
|
)
|
|
if (photoSelection) {
|
|
const context = photoSelection.context
|
|
editingContact.value = context?.contactId
|
|
? (calls.contacts.find((contact) => contact.id === context.contactId) ??
|
|
null)
|
|
: null
|
|
contactFirstName.value = context?.firstName ?? ''
|
|
contactLastName.value = context?.lastName ?? ''
|
|
contactEmail.value = context?.email ?? ''
|
|
contactNotes.value = context?.notes ?? ''
|
|
contactOrganization.value = context?.organization ?? ''
|
|
contactNumber.value = context?.phoneNumber ?? ''
|
|
contactAvatarMediaId.value = context?.avatarMediaId ?? null
|
|
contactAvatarUrl.value = context?.avatarUrl ?? ''
|
|
if (photoSelection.media[0]) {
|
|
contactAvatarMediaId.value = photoSelection.media[0].id
|
|
contactAvatarUrl.value = photoSelection.media[0].url
|
|
}
|
|
tab.value = 'contacts'
|
|
editorOpened.value = true
|
|
} else if (typeof route.query.contactId === 'string') {
|
|
const requestedContact = calls.contacts.find(
|
|
(contact) => contact.id === route.query.contactId,
|
|
)
|
|
if (requestedContact) {
|
|
tab.value = 'contacts'
|
|
openRecentDetail(requestedContact.phone_number)
|
|
}
|
|
await router.replace('/apps/phone')
|
|
} else if (typeof route.query.newContactNumber === 'string') {
|
|
tab.value = 'contacts'
|
|
openContact(undefined, route.query.newContactNumber)
|
|
await router.replace('/apps/phone')
|
|
}
|
|
callClock = window.setInterval(updateCallElapsed, 500)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('keydown', handleKeypadKeyboard)
|
|
if (callClock !== null) window.clearInterval(callClock)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<sky-app-page
|
|
class="native-app phone-calls-app"
|
|
:class="{
|
|
'phone-app--light': !phone.isDarkMode,
|
|
'phone-calls-app--profile': Boolean(selectedNumber),
|
|
}"
|
|
>
|
|
<template v-if="calls.activeCall">
|
|
<section
|
|
class="phone-active-call"
|
|
:class="{ 'phone-active-call--more': callMoreOpened }"
|
|
>
|
|
<header class="phone-active-call__identity">
|
|
<div class="phone-active-call__status">
|
|
<span>P</span>
|
|
{{ activeCallStatus }}
|
|
</div>
|
|
<h1>
|
|
{{ contactNameFor(calls.activeCall.otherNumber) }}
|
|
</h1>
|
|
<p v-if="error" class="phone-active-call__error">{{ error }}</p>
|
|
</header>
|
|
|
|
<Transition name="call-panel" mode="out-in">
|
|
<div
|
|
v-if="callKeypadOpened"
|
|
key="keypad"
|
|
class="phone-in-call-keypad"
|
|
>
|
|
<div class="phone-in-call-keypad__grid">
|
|
<button
|
|
v-for="key in keypadKeys"
|
|
:key="key.digit"
|
|
type="button"
|
|
@click="addInCallDigit(key.digit)"
|
|
>
|
|
<span>{{ key.digit }}</span>
|
|
<small>{{ key.letters }}</small>
|
|
</button>
|
|
</div>
|
|
<div class="phone-in-call-keypad__footer">
|
|
<sky-button
|
|
rounded
|
|
class="phone-in-call-keypad__end"
|
|
variant="danger"
|
|
@click="calls.hangup()"
|
|
>
|
|
<PhoneOff />
|
|
</sky-button>
|
|
<sky-button
|
|
clear
|
|
class="phone-in-call-keypad__hide"
|
|
@click="callKeypadOpened = false"
|
|
>{{ phone.t('Apps.phone.hideKeypad') }}</sky-button
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="callMoreOpened"
|
|
key="more"
|
|
class="phone-call-more"
|
|
@click.self="callMoreOpened = false"
|
|
>
|
|
<div class="phone-call-more__list">
|
|
<sky-button
|
|
class="phone-call-more__item phone-call-more__contact-card"
|
|
@click="openCallContact"
|
|
>
|
|
<span class="phone-call-more__avatar">
|
|
<span v-if="contactInitials(calls.activeCall.otherNumber)">
|
|
{{ contactInitials(calls.activeCall.otherNumber) }}
|
|
</span>
|
|
<UserRound v-else />
|
|
</span>
|
|
<strong>{{
|
|
phone.t(
|
|
activeCallContact
|
|
? 'Apps.phone.contactCard'
|
|
: 'Apps.phone.addToContacts',
|
|
)
|
|
}}</strong>
|
|
</sky-button>
|
|
|
|
<sky-button
|
|
class="phone-call-more__item"
|
|
@click="messageActiveCaller"
|
|
>
|
|
<MessageCircle />
|
|
<strong>{{ phone.t('Apps.phone.sendMessage') }}</strong>
|
|
</sky-button>
|
|
|
|
<sky-button
|
|
v-if="activeCallContact"
|
|
class="phone-call-more__item phone-call-more__item--danger"
|
|
@click="removeActiveCallerContact"
|
|
>
|
|
<Delete />
|
|
<strong>{{ phone.t('Apps.phone.removeContact') }}</strong>
|
|
</sky-button>
|
|
|
|
<sky-button
|
|
class="phone-call-more__item phone-call-more__item--danger"
|
|
@click="confirmBlockNumber(calls.activeCall.otherNumber)"
|
|
>
|
|
<PhoneOff />
|
|
<strong>{{ phone.t('Apps.phone.blockCaller') }}</strong>
|
|
</sky-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else key="actions" class="phone-active-call__actions">
|
|
<sky-button
|
|
rounded
|
|
class="phone-call-action"
|
|
:class="{
|
|
'is-active': calls.activeCall.speakerEnabled,
|
|
'is-disabled':
|
|
calls.activeCall.state !== 'connected' ||
|
|
!calls.activeCall.speakerSupported,
|
|
}"
|
|
:disabled="
|
|
calls.activeCall.state !== 'connected' ||
|
|
!calls.activeCall.speakerSupported ||
|
|
callSpeakerPending
|
|
"
|
|
:aria-pressed="calls.activeCall.speakerEnabled === true"
|
|
@click="toggleCallSpeaker"
|
|
>
|
|
<Volume2 />
|
|
<span>{{ phone.t('Apps.phone.speaker') }}</span>
|
|
</sky-button>
|
|
<sky-button rounded class="phone-call-action is-disabled" disabled>
|
|
<Video />
|
|
<span>{{ phone.t('Apps.phone.faceTime') }}</span>
|
|
</sky-button>
|
|
<sky-button
|
|
rounded
|
|
class="phone-call-action"
|
|
:class="{
|
|
'is-active': calls.activeCall.muted,
|
|
'is-disabled':
|
|
calls.activeCall.state !== 'connected' ||
|
|
!calls.activeCall.muteSupported,
|
|
}"
|
|
:disabled="
|
|
calls.activeCall.state !== 'connected' ||
|
|
!calls.activeCall.muteSupported ||
|
|
callMutePending
|
|
"
|
|
:aria-busy="callMutePending || undefined"
|
|
:aria-pressed="calls.activeCall.muted === true"
|
|
@click="toggleCallMute"
|
|
>
|
|
<MicOff />
|
|
<span>{{ phone.t('Apps.phone.mute') }}</span>
|
|
</sky-button>
|
|
|
|
<div class="phone-call-action-anchor">
|
|
<sky-button
|
|
rounded
|
|
class="phone-call-action"
|
|
@click="callMoreOpened = !callMoreOpened"
|
|
>
|
|
<MoreHorizontal />
|
|
<span>{{ phone.t('Apps.phone.more') }}</span>
|
|
</sky-button>
|
|
</div>
|
|
<sky-button
|
|
rounded
|
|
class="phone-call-action phone-call-action--end"
|
|
variant="danger"
|
|
@click="
|
|
calls.activeCall.direction === 'incoming' &&
|
|
calls.activeCall.state === 'ringing'
|
|
? calls.decline()
|
|
: calls.hangup()
|
|
"
|
|
>
|
|
<PhoneOff />
|
|
<span>{{
|
|
phone.t(
|
|
calls.activeCall.direction === 'incoming' &&
|
|
calls.activeCall.state === 'ringing'
|
|
? 'Apps.phone.decline'
|
|
: 'Apps.phone.hangup',
|
|
)
|
|
}}</span>
|
|
</sky-button>
|
|
<sky-button
|
|
v-if="
|
|
calls.activeCall.direction === 'incoming' &&
|
|
calls.activeCall.state === 'ringing'
|
|
"
|
|
rounded
|
|
class="phone-call-action phone-call-action--answer"
|
|
@click="answerCall"
|
|
>
|
|
<Phone />
|
|
<span>{{ phone.t('Apps.phone.answer') }}</span>
|
|
</sky-button>
|
|
<sky-button
|
|
v-else
|
|
rounded
|
|
class="phone-call-action"
|
|
@click="openCallKeypad"
|
|
>
|
|
<Grid3X3 />
|
|
<span>{{ phone.t('Apps.phone.keypad') }}</span>
|
|
</sky-button>
|
|
</div>
|
|
</Transition>
|
|
</section>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div
|
|
ref="phoneContent"
|
|
class="phone-call-content"
|
|
:class="{ 'phone-call-content--profile': selectedNumber }"
|
|
>
|
|
<sky-block v-if="!phone.device?.sim" class="text-center">
|
|
<h2>{{ phone.t('Apps.phone.noSim') }}</h2>
|
|
<p class="text-[#8e8e93]">{{ phone.t('Apps.phone.noSimBody') }}</p>
|
|
</sky-block>
|
|
|
|
<template v-else-if="selectedNumber">
|
|
<section class="phone-contact-detail">
|
|
<header class="phone-detail-header">
|
|
<sky-button
|
|
rounded
|
|
class="phone-detail-header-button phone-detail-back"
|
|
:aria-label="phone.t('Common.back')"
|
|
@click="closeRecentDetail"
|
|
>
|
|
<ChevronLeft :size="29" :stroke-width="2.4" />
|
|
</sky-button>
|
|
|
|
<span class="phone-detail-header-spacer" />
|
|
|
|
<sky-button
|
|
v-if="!viewingOwnCard"
|
|
rounded
|
|
class="phone-detail-edit"
|
|
@click="
|
|
openContact(selectedContact ?? undefined, selectedNumber)
|
|
"
|
|
>
|
|
{{
|
|
phone.t(
|
|
selectedContact ? 'Common.edit' : 'Apps.phone.addContact',
|
|
)
|
|
}}
|
|
</sky-button>
|
|
<span v-else class="phone-detail-edit-spacer" />
|
|
</header>
|
|
|
|
<div class="phone-contact-hero">
|
|
<div class="phone-contact-avatar phone-contact-avatar--large">
|
|
<img
|
|
v-if="selectedContact?.avatar_url"
|
|
:src="selectedContact.avatar_url"
|
|
alt=""
|
|
/>
|
|
<span v-else-if="contactInitials(selectedNumber)">{{
|
|
contactInitials(selectedNumber)
|
|
}}</span>
|
|
<UserRound v-else :size="48" :stroke-width="1.8" />
|
|
</div>
|
|
<h2>{{ selectedDisplayName }}</h2>
|
|
<p
|
|
v-if="selectedContact?.organization"
|
|
class="phone-contact-organization"
|
|
>
|
|
{{ selectedContact.organization }}
|
|
</p>
|
|
<p v-if="!selectedContact && !viewingOwnCard">
|
|
{{ phone.t('Apps.phone.unknownCaller') }}
|
|
</p>
|
|
|
|
<div class="phone-contact-actions">
|
|
<sky-button
|
|
v-for="action in contactProfileActions"
|
|
:key="action.id"
|
|
rounded
|
|
class="phone-profile-action"
|
|
:disabled="
|
|
viewingOwnCard ||
|
|
['video', 'mail'].includes(action.id) ||
|
|
(action.id === 'call' &&
|
|
selectedContact?.canCall === false) ||
|
|
(action.id === 'message' &&
|
|
selectedContact?.canMessage === false)
|
|
"
|
|
:aria-label="phone.t(`Apps.phone.${action.id}`)"
|
|
@click="triggerContactAction(action.id)"
|
|
>
|
|
<component :is="action.icon" :size="23" fill="currentColor" />
|
|
</sky-button>
|
|
</div>
|
|
</div>
|
|
|
|
<section v-if="viewingOwnCard" class="phone-profile-content">
|
|
<sky-glass class="phone-own-profile-card">
|
|
<div>
|
|
<span><Phone :size="19" /></span>
|
|
<p>
|
|
<small>{{ phone.t('Apps.phone.myNumber') }}</small>
|
|
<strong>{{ formatPhoneNumber(selectedNumber) }}</strong>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span><Smartphone :size="19" /></span>
|
|
<p>
|
|
<small>{{ phone.t('Apps.phone.device') }}</small>
|
|
<strong>{{ phone.device?.name }}</strong>
|
|
</p>
|
|
</div>
|
|
</sky-glass>
|
|
<sky-glass class="phone-profile-card phone-profile-single-option">
|
|
<button type="button" @click="shareOwnProfile">
|
|
<span>{{ phone.t('Apps.easyShare.shareProfile') }}</span>
|
|
</button>
|
|
</sky-glass>
|
|
</section>
|
|
|
|
<section v-else class="phone-profile-content">
|
|
<sky-glass class="phone-profile-card phone-profile-info-card">
|
|
<button
|
|
type="button"
|
|
:disabled="selectedContact?.canCall === false"
|
|
@click="startCall(selectedNumber)"
|
|
>
|
|
<span>
|
|
<small>{{ phone.t('Apps.phone.mobile') }}</small>
|
|
<strong>{{ formatPhoneNumber(selectedNumber) }}</strong>
|
|
</span>
|
|
<Phone :size="21" fill="currentColor" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
:disabled="selectedContact?.canMessage === false"
|
|
@click="openMessage(selectedNumber)"
|
|
>
|
|
<span>
|
|
<small>{{ phone.t('Apps.phone.messagesProfile') }}</small>
|
|
<strong>{{ selectedDisplayName }}</strong>
|
|
</span>
|
|
<ChevronRight :size="23" />
|
|
</button>
|
|
<div class="phone-profile-notes">
|
|
<span>{{ phone.t('Apps.phone.notes') }}</span>
|
|
<p v-if="selectedContact?.notes">
|
|
{{ selectedContact.notes }}
|
|
</p>
|
|
</div>
|
|
</sky-glass>
|
|
|
|
<sky-glass class="phone-profile-card phone-profile-options-card">
|
|
<button
|
|
type="button"
|
|
:disabled="selectedContact?.canMessage === false"
|
|
@click="openMessage(selectedNumber)"
|
|
>
|
|
<span>{{ phone.t('Apps.phone.sendMessage') }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
:disabled="!selectedContact"
|
|
@click="shareSelectedContact"
|
|
>
|
|
<span>{{ phone.t('Apps.phone.shareContact') }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
:disabled="!selectedContact"
|
|
@click="toggleSelectedFavorite"
|
|
>
|
|
<span>{{
|
|
phone.t(
|
|
selectedContact?.favorite
|
|
? 'Apps.phone.removeFavorite'
|
|
: 'Apps.phone.addFavorite',
|
|
)
|
|
}}</span>
|
|
</button>
|
|
</sky-glass>
|
|
|
|
<sky-glass class="phone-profile-card phone-profile-single-option">
|
|
<button
|
|
type="button"
|
|
@click="confirmBlockNumber(selectedNumber)"
|
|
>
|
|
<span>{{ phone.t('Apps.phone.blockContact') }}</span>
|
|
</button>
|
|
</sky-glass>
|
|
|
|
<sky-glass class="phone-history-card">
|
|
<h3>{{ phone.t('Apps.phone.callHistory') }}</h3>
|
|
<div
|
|
v-for="recent in selectedHistory"
|
|
:key="recent.id"
|
|
class="phone-history-row"
|
|
>
|
|
<span
|
|
class="phone-history-direction"
|
|
:class="{
|
|
'phone-history-direction--missed': [
|
|
'missed',
|
|
'no_answer',
|
|
].includes(recent.status),
|
|
}"
|
|
>
|
|
<PhoneIncoming
|
|
v-if="recent.direction === 'incoming'"
|
|
:size="17"
|
|
/>
|
|
<PhoneOutgoing v-else :size="17" />
|
|
</span>
|
|
<span class="phone-history-copy">
|
|
<strong>{{ recentStatus(recent) }}</strong>
|
|
<small>{{ recentSubtitle(recent) }}</small>
|
|
</span>
|
|
<time>{{ formatRecentDate(recent.created_at) }}</time>
|
|
</div>
|
|
<p v-if="!selectedHistory.length" class="phone-history-empty">
|
|
{{ phone.t('Apps.phone.noCallHistory') }}
|
|
</p>
|
|
</sky-glass>
|
|
</section>
|
|
</section>
|
|
</template>
|
|
|
|
<template v-else-if="tab === 'recents'">
|
|
<section class="phone-recents">
|
|
<header class="phone-recents-header">
|
|
<div class="phone-recents-filter" role="group">
|
|
<button
|
|
type="button"
|
|
:class="{ active: recentFilter === 'all' }"
|
|
@click="recentFilter = 'all'"
|
|
>
|
|
{{ phone.t('Apps.phone.allCalls') }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
:class="{ active: recentFilter === 'missed' }"
|
|
@click="recentFilter = 'missed'"
|
|
>
|
|
{{ phone.t('Apps.phone.missedCalls') }}
|
|
</button>
|
|
</div>
|
|
<h1>{{ phone.t('Apps.phone.recents') }}</h1>
|
|
<label class="phone-recents-search">
|
|
<Search :size="21" />
|
|
<input
|
|
:value="recentQuery"
|
|
:placeholder="phone.t('Apps.phone.searchRecents')"
|
|
type="search"
|
|
@input="recentQuery = eventValue($event)"
|
|
/>
|
|
</label>
|
|
</header>
|
|
|
|
<div v-if="visibleRecents.length" class="phone-recents-list">
|
|
<article
|
|
v-for="recent in visibleRecents"
|
|
:key="recent.id"
|
|
class="phone-recent-row"
|
|
>
|
|
<div class="phone-contact-avatar">
|
|
<img
|
|
v-if="contactsByNumber.get(recent.other_number)?.avatar_url"
|
|
:src="
|
|
contactsByNumber.get(recent.other_number)?.avatar_url ??
|
|
''
|
|
"
|
|
alt=""
|
|
/>
|
|
<span v-else-if="contactInitials(recent.other_number)">{{
|
|
contactInitials(recent.other_number)
|
|
}}</span>
|
|
<UserRound v-else :size="30" :stroke-width="1.8" />
|
|
</div>
|
|
<button
|
|
class="phone-recent-call"
|
|
type="button"
|
|
@click="startCall(recent.other_number)"
|
|
>
|
|
<span
|
|
class="phone-recent-name"
|
|
:class="{
|
|
'phone-recent-name--missed': [
|
|
'missed',
|
|
'no_answer',
|
|
].includes(recent.status),
|
|
}"
|
|
>
|
|
{{ contactNameFor(recent.other_number) }}
|
|
</span>
|
|
<span class="phone-recent-meta">
|
|
<PhoneIncoming
|
|
v-if="recent.direction === 'incoming'"
|
|
:size="14"
|
|
/>
|
|
<PhoneOutgoing v-else :size="14" />
|
|
{{ recentStatus(recent) }}
|
|
</span>
|
|
</button>
|
|
<time class="phone-recent-date">{{
|
|
formatRecentDate(recent.created_at)
|
|
}}</time>
|
|
<button
|
|
class="phone-recent-info"
|
|
type="button"
|
|
:aria-label="phone.t('Apps.phone.contactDetails')"
|
|
@click="openRecentDetail(recent.other_number)"
|
|
>
|
|
<Info :size="21" />
|
|
</button>
|
|
</article>
|
|
</div>
|
|
<sky-block v-else class="text-center text-[#8e8e93]">{{
|
|
phone.t('Apps.phone.noRecents')
|
|
}}</sky-block>
|
|
</section>
|
|
</template>
|
|
|
|
<template v-else-if="tab === 'contacts'">
|
|
<section class="phone-contacts">
|
|
<header class="phone-contacts-header">
|
|
<div class="phone-contacts-toolbar">
|
|
<span aria-hidden="true" />
|
|
<h1>{{ phone.t('Apps.phone.contacts') }}</h1>
|
|
<button
|
|
class="phone-contacts-add"
|
|
type="button"
|
|
:aria-label="phone.t('Apps.phone.addContact')"
|
|
@click="openContact()"
|
|
>
|
|
<Plus :size="28" />
|
|
</button>
|
|
</div>
|
|
|
|
<label class="phone-contacts-search">
|
|
<Search :size="21" />
|
|
<input
|
|
:value="query"
|
|
:placeholder="phone.t('Common.search')"
|
|
type="search"
|
|
@input="query = eventValue($event)"
|
|
/>
|
|
</label>
|
|
</header>
|
|
|
|
<button class="phone-my-card" type="button" @click="openMyCard">
|
|
<div class="phone-contact-avatar">
|
|
<UserRound :size="30" :stroke-width="1.8" />
|
|
</div>
|
|
<span>
|
|
<strong>{{ phone.t('Apps.phone.myCard') }}</strong>
|
|
<small v-if="phone.device?.sim">{{
|
|
formatPhoneNumber(phone.device.sim.number)
|
|
}}</small>
|
|
</span>
|
|
</button>
|
|
|
|
<div v-if="groupedContacts.length" class="phone-contact-groups">
|
|
<section
|
|
v-if="!query && favoriteContacts.length"
|
|
id="phone-contact-group-favorites"
|
|
class="phone-contact-group phone-contact-group--favorites"
|
|
>
|
|
<h2>
|
|
<Star :size="13" fill="currentColor" />
|
|
{{ phone.t('Apps.phone.favorites') }}
|
|
</h2>
|
|
<button
|
|
v-for="contact in favoriteContacts"
|
|
:key="`favorite-${contact.id}`"
|
|
class="phone-contact-row"
|
|
type="button"
|
|
@click="openRecentDetail(contact.phone_number)"
|
|
>
|
|
<span class="phone-contact-avatar">
|
|
<img
|
|
v-if="contact.avatar_url"
|
|
:src="contact.avatar_url"
|
|
alt=""
|
|
/>
|
|
<template v-else>{{
|
|
contactInitials(contact.phone_number)
|
|
}}</template>
|
|
</span>
|
|
<span class="phone-contact-name">
|
|
{{ contact.name }}
|
|
<small v-if="contact.readonly">{{
|
|
phone.t('Apps.phone.officialContact')
|
|
}}</small>
|
|
</span>
|
|
</button>
|
|
</section>
|
|
<section
|
|
v-for="group in groupedContacts"
|
|
:id="`phone-contact-group-${
|
|
group.letter === '#' ? 'other' : group.letter.toLowerCase()
|
|
}`"
|
|
:key="group.letter"
|
|
class="phone-contact-group"
|
|
>
|
|
<h2>{{ group.letter }}</h2>
|
|
<button
|
|
v-for="contact in group.contacts"
|
|
:key="contact.id"
|
|
class="phone-contact-row"
|
|
type="button"
|
|
@click="openRecentDetail(contact.phone_number)"
|
|
>
|
|
<span class="phone-contact-avatar">
|
|
<img
|
|
v-if="contact.avatar_url"
|
|
:src="contact.avatar_url"
|
|
alt=""
|
|
/>
|
|
<template v-else>{{
|
|
contactInitials(contact.phone_number)
|
|
}}</template>
|
|
</span>
|
|
<span class="phone-contact-name">
|
|
{{ contact.name }}
|
|
<small v-if="contact.readonly">{{
|
|
phone.t('Apps.phone.officialContact')
|
|
}}</small>
|
|
</span>
|
|
</button>
|
|
</section>
|
|
</div>
|
|
<sky-block v-else class="text-center text-[#8e8e93]">{{
|
|
phone.t('Apps.phone.noContacts')
|
|
}}</sky-block>
|
|
|
|
<nav
|
|
v-if="!query"
|
|
class="phone-contact-index"
|
|
:aria-label="phone.t('Apps.phone.contactIndex')"
|
|
>
|
|
<button
|
|
v-if="favoriteContacts.length"
|
|
type="button"
|
|
:aria-label="phone.t('Apps.phone.favorites')"
|
|
@click="scrollToContactGroup('favorites')"
|
|
>
|
|
<Star :size="9" fill="currentColor" />
|
|
</button>
|
|
<button
|
|
v-for="letter in contactAlphabet"
|
|
:key="letter"
|
|
type="button"
|
|
@click="scrollToContactGroup(letter)"
|
|
>
|
|
{{ letter }}
|
|
</button>
|
|
</nav>
|
|
</section>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<section class="phone-keypad">
|
|
<Transition name="keypad-suggestions">
|
|
<div
|
|
v-if="keypadSuggestions.length"
|
|
class="phone-keypad-suggestions"
|
|
aria-live="polite"
|
|
>
|
|
<button
|
|
v-for="contact in keypadSuggestions"
|
|
:key="contact.id"
|
|
class="phone-keypad-suggestion"
|
|
type="button"
|
|
@click="chooseKeypadSuggestion(contact)"
|
|
>
|
|
<span class="phone-keypad-suggestion__avatar">
|
|
<img
|
|
v-if="contact.avatar_url"
|
|
:src="contact.avatar_url"
|
|
alt=""
|
|
/>
|
|
<template v-else>{{
|
|
contactInitials(contact.phone_number)
|
|
}}</template>
|
|
</span>
|
|
<span class="phone-keypad-suggestion__copy">
|
|
<strong>{{ contact.name }}</strong>
|
|
<small>{{ formatPhoneNumber(contact.phone_number) }}</small>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</Transition>
|
|
<div class="phone-keypad-number" aria-live="polite">
|
|
{{ keypadDisplay }}
|
|
</div>
|
|
<div class="phone-keypad-grid">
|
|
<button
|
|
v-for="key in keypadKeys"
|
|
:key="key.digit"
|
|
class="phone-keypad-key"
|
|
type="button"
|
|
@click="addDigit(key.digit)"
|
|
>
|
|
<span>{{ key.digit }}</span>
|
|
<small :class="{ 'phone-keypad-plus': key.digit === '0' }">
|
|
{{ key.letters }}
|
|
</small>
|
|
</button>
|
|
</div>
|
|
<div class="phone-keypad-actions">
|
|
<button
|
|
class="phone-keypad-call"
|
|
type="button"
|
|
@click="startCall(keypad)"
|
|
>
|
|
<Phone :size="31" fill="currentColor" />
|
|
</button>
|
|
<button
|
|
v-if="keypad"
|
|
class="phone-keypad-delete"
|
|
type="button"
|
|
:aria-label="phone.t('Common.delete')"
|
|
@click="keypad = keypad.slice(0, -1)"
|
|
>
|
|
<Delete :size="27" />
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<p v-if="error" class="px-5 text-center text-sm text-[#ff3b30]">
|
|
{{ error }}
|
|
</p>
|
|
</div>
|
|
<sky-tab-bar
|
|
icons
|
|
labels
|
|
class="phone-bottom-tabbar"
|
|
:aria-label="phone.t('Apps.phone.name')"
|
|
>
|
|
<sky-tab-button
|
|
v-for="item in tabs"
|
|
:key="item.id"
|
|
:active="tab === item.id"
|
|
:aria-label="phone.t(`Apps.phone.${item.id}`)"
|
|
@click="selectTab(item.id)"
|
|
>
|
|
<template #icon>
|
|
<component :is="item.icon" aria-hidden="true" />
|
|
</template>
|
|
<template #label>{{ phone.t(`Apps.phone.${item.id}`) }}</template>
|
|
</sky-tab-button>
|
|
</sky-tab-bar>
|
|
</template>
|
|
</sky-app-page>
|
|
|
|
<div class="phone-contact-editor-sheet">
|
|
<sky-sheet :opened="editorOpened" @backdropclick="editorOpened = false">
|
|
<section
|
|
class="phone-contact-editor"
|
|
:class="{ 'phone-contact-editor--light': !phone.isDarkMode }"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
:aria-label="
|
|
phone.t(
|
|
editingContact?.readonly
|
|
? 'Apps.phone.officialContact'
|
|
: editingContact
|
|
? 'Apps.phone.editContact'
|
|
: 'Apps.phone.addContact',
|
|
)
|
|
"
|
|
>
|
|
<header class="phone-contact-editor__header">
|
|
<sky-button
|
|
rounded
|
|
class="phone-contact-editor__header-button"
|
|
:aria-label="phone.t('Common.cancel')"
|
|
@click="editorOpened = false"
|
|
>
|
|
<X :size="27" />
|
|
</sky-button>
|
|
<h2>
|
|
{{
|
|
phone.t(
|
|
editingContact?.readonly
|
|
? 'Apps.phone.officialContact'
|
|
: editingContact
|
|
? 'Apps.phone.editContact'
|
|
: 'Apps.phone.newContact',
|
|
)
|
|
}}
|
|
</h2>
|
|
<sky-button
|
|
v-if="!editingContact?.readonly"
|
|
rounded
|
|
class="phone-contact-editor__header-button"
|
|
:aria-label="phone.t('Common.save')"
|
|
@click="saveContact"
|
|
>
|
|
<Check :size="28" :stroke-width="2.2" />
|
|
</sky-button>
|
|
</header>
|
|
|
|
<div class="phone-contact-editor__scroll">
|
|
<div class="phone-contact-editor__photo">
|
|
<div
|
|
class="phone-contact-editor__avatar-wrap"
|
|
:class="{
|
|
'phone-contact-editor__avatar-wrap--has-photo':
|
|
contactAvatarUrl,
|
|
}"
|
|
>
|
|
<button
|
|
class="phone-contact-editor__avatar"
|
|
type="button"
|
|
:aria-label="phone.t('Apps.phone.choosePhoto')"
|
|
:disabled="editingContact?.readonly"
|
|
@click="openContactPhotoPicker('photos')"
|
|
>
|
|
<img v-if="contactAvatarUrl" :src="contactAvatarUrl" alt="" />
|
|
<span v-else-if="contactEditorInitials">{{
|
|
contactEditorInitials
|
|
}}</span>
|
|
<UserRound v-else :size="72" :stroke-width="1.35" />
|
|
</button>
|
|
<sky-button
|
|
v-if="contactAvatarUrl && !editingContact?.readonly"
|
|
rounded
|
|
class="phone-contact-editor__remove-photo"
|
|
:aria-label="phone.t('Apps.phone.removePhoto')"
|
|
@click="removeContactPhoto"
|
|
>
|
|
<Delete :size="25" />
|
|
<span>{{ phone.t('Apps.phone.removePhoto') }}</span>
|
|
</sky-button>
|
|
</div>
|
|
<div
|
|
v-if="!editingContact?.readonly"
|
|
class="phone-contact-editor__photo-actions"
|
|
>
|
|
<sky-button
|
|
small
|
|
rounded
|
|
tonal
|
|
@click="openContactPhotoPicker('photos')"
|
|
>
|
|
<Images :size="18" />
|
|
{{ phone.t('Apps.phone.chooseGallery') }}
|
|
</sky-button>
|
|
<sky-button
|
|
small
|
|
rounded
|
|
tonal
|
|
@click="openContactPhotoPicker('camera')"
|
|
>
|
|
<Camera :size="18" />
|
|
{{ phone.t('Apps.phone.takePhoto') }}
|
|
</sky-button>
|
|
</div>
|
|
</div>
|
|
|
|
<sky-list strong inset class="phone-contact-editor__name-list">
|
|
<sky-field
|
|
:value="contactFirstName"
|
|
:placeholder="phone.t('Apps.phone.firstName')"
|
|
autocomplete="given-name"
|
|
:readonly="editingContact?.readonly"
|
|
@input="contactFirstName = eventValue($event)"
|
|
/>
|
|
<sky-field
|
|
:value="contactLastName"
|
|
:placeholder="phone.t('Apps.phone.lastName')"
|
|
autocomplete="family-name"
|
|
:readonly="editingContact?.readonly"
|
|
@input="contactLastName = eventValue($event)"
|
|
/>
|
|
<sky-field
|
|
:value="contactOrganization"
|
|
:placeholder="phone.t('Apps.phone.companyOrGroup')"
|
|
autocomplete="organization"
|
|
:readonly="editingContact?.readonly"
|
|
@input="contactOrganization = eventValue($event)"
|
|
/>
|
|
<sky-field
|
|
:value="contactEmail"
|
|
type="email"
|
|
:placeholder="phone.t('Apps.phone.mail')"
|
|
autocomplete="email"
|
|
:readonly="editingContact?.readonly"
|
|
@input="contactEmail = eventValue($event)"
|
|
/>
|
|
</sky-list>
|
|
|
|
<sky-list strong inset class="phone-contact-editor__number-list">
|
|
<sky-field
|
|
:value="contactNumber"
|
|
:label="phone.t('Apps.phone.mobile')"
|
|
:placeholder="phone.t('Apps.phone.phoneNumber')"
|
|
inputmode="tel"
|
|
autocomplete="tel"
|
|
:readonly="editingContact?.readonly"
|
|
@input="contactNumber = eventValue($event)"
|
|
>
|
|
<template #media>
|
|
<span class="phone-contact-editor__add-icon"
|
|
><Plus :size="19"
|
|
/></span>
|
|
</template>
|
|
</sky-field>
|
|
</sky-list>
|
|
|
|
<sky-list strong inset class="phone-contact-editor__notes-list">
|
|
<sky-field
|
|
:value="contactNotes"
|
|
type="textarea"
|
|
:placeholder="phone.t('Apps.phone.notes')"
|
|
:maxlength="500"
|
|
autocapitalize="sentences"
|
|
:readonly="editingContact?.readonly"
|
|
@input="contactNotes = eventValue($event)"
|
|
/>
|
|
</sky-list>
|
|
|
|
<p v-if="error" class="phone-contact-editor__error">{{ error }}</p>
|
|
|
|
<sky-button
|
|
v-if="editingContact && !editingContact.readonly"
|
|
class="phone-contact-editor__delete"
|
|
@click="deleteEditedContact"
|
|
>
|
|
{{ phone.t('Apps.phone.deleteContact') }}
|
|
</sky-button>
|
|
</div>
|
|
</section>
|
|
</sky-sheet>
|
|
</div>
|
|
|
|
<sky-dialog
|
|
:opened="blockDialogOpened"
|
|
@backdropclick="blockDialogOpened = false"
|
|
>
|
|
<template #title>{{ phone.t('Apps.phone.blockCallerTitle') }}</template>
|
|
<p>
|
|
{{
|
|
phone.t('Apps.phone.blockCallerBody', {
|
|
number: contactNameFor(blockTargetNumber),
|
|
})
|
|
}}
|
|
</p>
|
|
<template #buttons>
|
|
<sky-dialog-button @click="blockDialogOpened = false">{{
|
|
phone.t('Common.cancel')
|
|
}}</sky-dialog-button>
|
|
<sky-dialog-button strong @click="blockNumber">{{
|
|
phone.t('Apps.phone.block')
|
|
}}</sky-dialog-button>
|
|
</template>
|
|
</sky-dialog>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.phone-active-call {
|
|
position: absolute;
|
|
z-index: 30;
|
|
inset: 0;
|
|
display: flex;
|
|
min-height: 0;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
background: linear-gradient(180deg, #2f4a98 0%, #2a468f 44%, #293044 100%);
|
|
color: white;
|
|
text-align: center;
|
|
}
|
|
|
|
.phone-active-call::before {
|
|
position: absolute;
|
|
z-index: 0;
|
|
inset: 0;
|
|
background: rgba(4, 12, 30, 0.48);
|
|
content: '';
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 260ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
.phone-active-call > * {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.phone-active-call--more::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.phone-active-call--more .phone-active-call__identity {
|
|
opacity: 0.48;
|
|
}
|
|
|
|
.phone-active-call__identity {
|
|
padding: 112px 24px 0;
|
|
transition: opacity 240ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
.call-panel-enter-active {
|
|
transition:
|
|
opacity 240ms ease-out,
|
|
transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
will-change: opacity, transform;
|
|
}
|
|
|
|
.call-panel-leave-active {
|
|
transition:
|
|
opacity 150ms ease-in,
|
|
transform 190ms cubic-bezier(0.4, 0, 1, 1);
|
|
will-change: opacity, transform;
|
|
}
|
|
|
|
.call-panel-enter-from,
|
|
.call-panel-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.phone-active-call__actions.call-panel-enter-from,
|
|
.phone-active-call__actions.call-panel-leave-to {
|
|
transform: translateY(18px) scale(0.985);
|
|
}
|
|
|
|
.phone-call-more.call-panel-enter-from,
|
|
.phone-call-more.call-panel-leave-to {
|
|
transform: translateY(38px);
|
|
}
|
|
|
|
.phone-in-call-keypad.call-panel-enter-from,
|
|
.phone-in-call-keypad.call-panel-leave-to {
|
|
transform: translateY(24px) scale(0.97);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.phone-active-call::before,
|
|
.phone-active-call__identity,
|
|
.call-panel-enter-active,
|
|
.call-panel-leave-active {
|
|
transition-duration: 1ms !important;
|
|
}
|
|
}
|
|
|
|
.phone-active-call__status {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 7px;
|
|
color: rgba(224, 230, 255, 0.66);
|
|
font-size: 25px;
|
|
font-variant-numeric: tabular-nums;
|
|
font-weight: 500;
|
|
line-height: 28px;
|
|
}
|
|
|
|
.phone-active-call__status span {
|
|
display: grid;
|
|
width: 16px;
|
|
height: 16px;
|
|
place-items: center;
|
|
border-radius: 3px;
|
|
background: white;
|
|
color: #172758;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
line-height: 1;
|
|
}
|
|
|
|
.phone-active-call__identity h1 {
|
|
max-width: 100%;
|
|
margin: 9px 0 0;
|
|
overflow: hidden;
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.8px;
|
|
line-height: 1.08;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-active-call__error {
|
|
margin: 10px 0 0;
|
|
color: #ffb4ae;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.phone-active-call__actions {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 11px 19px;
|
|
width: 100%;
|
|
margin-top: auto;
|
|
padding: 0 28px 48px;
|
|
}
|
|
|
|
.phone-call-action {
|
|
position: relative;
|
|
display: flex !important;
|
|
width: 100% !important;
|
|
height: 108px !important;
|
|
flex-direction: column !important;
|
|
align-items: center !important;
|
|
justify-content: flex-start !important;
|
|
gap: 13px !important;
|
|
padding: 24px 0 0 !important;
|
|
border: 0 !important;
|
|
background: transparent !important;
|
|
box-shadow: none !important;
|
|
color: white !important;
|
|
overflow: visible !important;
|
|
font-size: 14px !important;
|
|
font-weight: 400 !important;
|
|
line-height: 17px !important;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-call-action-anchor {
|
|
min-width: 0;
|
|
height: 108px;
|
|
}
|
|
|
|
.phone-call-action::before {
|
|
position: absolute;
|
|
z-index: 0;
|
|
top: 0;
|
|
left: 50%;
|
|
width: 80px;
|
|
height: 80px;
|
|
border: 1px solid rgba(215, 225, 255, 0.72);
|
|
border-radius: 50%;
|
|
background: rgba(145, 164, 215, 0.22);
|
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
content: '';
|
|
transform: translateX(-50%);
|
|
transition:
|
|
background-color 140ms ease,
|
|
transform 140ms ease;
|
|
}
|
|
|
|
.phone-call-action:active::before {
|
|
transform: translateX(-50%) scale(0.94);
|
|
}
|
|
|
|
.phone-call-action > svg,
|
|
.phone-call-action > span {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.phone-call-action > svg {
|
|
width: 32px;
|
|
height: 32px;
|
|
flex: none;
|
|
stroke-width: 2.2;
|
|
}
|
|
|
|
.phone-call-action > span {
|
|
position: absolute;
|
|
top: 89px;
|
|
display: block;
|
|
width: 112px;
|
|
overflow: hidden;
|
|
color: #fff;
|
|
line-height: 18px;
|
|
text-overflow: ellipsis;
|
|
transition: none;
|
|
}
|
|
|
|
.phone-call-action.is-active {
|
|
color: #25458e !important;
|
|
}
|
|
|
|
.phone-call-action.is-active::before {
|
|
background: white;
|
|
}
|
|
|
|
.phone-call-action.is-disabled {
|
|
color: rgba(232, 236, 250, 0.56) !important;
|
|
opacity: 1 !important;
|
|
}
|
|
|
|
.phone-call-action.is-disabled > span {
|
|
color: rgba(232, 236, 250, 0.56);
|
|
}
|
|
|
|
.phone-call-action--end::before {
|
|
border-color: rgba(255, 186, 181, 0.72);
|
|
background: #d92d22;
|
|
}
|
|
|
|
.phone-call-action--answer::before {
|
|
border-color: rgba(190, 255, 198, 0.72);
|
|
background: #34a853;
|
|
}
|
|
|
|
.phone-call-more {
|
|
display: flex;
|
|
min-height: 0;
|
|
flex: 1;
|
|
align-items: flex-end;
|
|
padding: 0 34px 34px;
|
|
}
|
|
|
|
.phone-call-more__list {
|
|
display: flex;
|
|
width: 100%;
|
|
flex-direction: column;
|
|
gap: 9px;
|
|
}
|
|
|
|
.phone-call-more__item {
|
|
display: flex !important;
|
|
width: 100% !important;
|
|
height: 58px !important;
|
|
align-items: center !important;
|
|
justify-content: flex-start !important;
|
|
gap: 13px !important;
|
|
padding: 0 27px !important;
|
|
border: 1px solid rgba(118, 137, 191, 0.38) !important;
|
|
border-radius: 30px !important;
|
|
background: rgba(20, 23, 39, 0.55) !important;
|
|
color: white !important;
|
|
box-shadow: none !important;
|
|
text-transform: none !important;
|
|
}
|
|
|
|
.phone-call-more__item:active {
|
|
background: rgba(49, 57, 84, 0.68) !important;
|
|
}
|
|
|
|
.phone-call-more__contact-card {
|
|
height: 72px !important;
|
|
padding: 0 33px !important;
|
|
border-radius: 38px !important;
|
|
}
|
|
|
|
.phone-call-more__item > svg {
|
|
width: 24px;
|
|
height: 24px;
|
|
flex: none;
|
|
}
|
|
|
|
.phone-call-more__item--danger {
|
|
color: #ff6961 !important;
|
|
}
|
|
|
|
.phone-call-more__avatar {
|
|
display: grid;
|
|
width: 45px;
|
|
height: 45px;
|
|
flex: none;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
background: linear-gradient(145deg, #4b92d1, #195287);
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.phone-call-more__avatar svg {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.phone-call-more__item strong {
|
|
overflow: hidden;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-in-call-keypad {
|
|
display: flex;
|
|
min-height: 0;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
padding: 0 28px 42px;
|
|
}
|
|
|
|
.phone-in-call-keypad__grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 74px);
|
|
justify-content: center;
|
|
gap: 18px 40px;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.phone-in-call-keypad__grid button {
|
|
display: flex;
|
|
width: 74px;
|
|
height: 74px;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
border-radius: 50%;
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(255, 255, 255, 0.16),
|
|
rgba(255, 255, 255, 0.09)
|
|
);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.14),
|
|
0 5px 14px rgba(8, 20, 55, 0.2);
|
|
backdrop-filter: blur(18px) saturate(145%);
|
|
-webkit-backdrop-filter: blur(18px) saturate(145%);
|
|
color: white;
|
|
transition:
|
|
background-color 120ms ease,
|
|
border-color 120ms ease,
|
|
box-shadow 120ms ease,
|
|
transform 120ms ease;
|
|
}
|
|
|
|
.phone-in-call-keypad__grid button:active {
|
|
background: rgba(151, 176, 231, 0.44);
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.phone-in-call-keypad__grid span {
|
|
font-size: 32px;
|
|
line-height: 31px;
|
|
}
|
|
|
|
.phone-in-call-keypad__grid small {
|
|
min-height: 10px;
|
|
margin-top: 2px;
|
|
font-size: 8px;
|
|
font-weight: 800;
|
|
letter-spacing: 1.5px;
|
|
}
|
|
|
|
.phone-in-call-keypad__footer {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 74px);
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 40px;
|
|
margin-top: 42px;
|
|
}
|
|
|
|
.phone-in-call-keypad__end {
|
|
grid-column: 2;
|
|
width: 74px !important;
|
|
height: 74px !important;
|
|
padding: 0 !important;
|
|
border: 1px solid rgba(255, 178, 172, 0.78) !important;
|
|
border-radius: 50% !important;
|
|
background: #cf3027 !important;
|
|
color: white !important;
|
|
}
|
|
|
|
.phone-in-call-keypad__end svg {
|
|
width: 30px;
|
|
height: 30px;
|
|
}
|
|
|
|
.phone-in-call-keypad__hide {
|
|
grid-column: 3;
|
|
width: 96px !important;
|
|
margin-left: -11px;
|
|
padding: 0 !important;
|
|
color: white !important;
|
|
font-size: 14px !important;
|
|
font-weight: 400 !important;
|
|
line-height: 18px !important;
|
|
text-align: left;
|
|
white-space: normal !important;
|
|
}
|
|
|
|
@media (max-height: 700px) {
|
|
.phone-active-call__identity {
|
|
padding-top: 86px;
|
|
}
|
|
|
|
.phone-active-call__actions {
|
|
gap: 4px 15px;
|
|
padding-bottom: 28px;
|
|
}
|
|
|
|
.phone-call-action {
|
|
height: 97px !important;
|
|
padding-top: 20px !important;
|
|
}
|
|
|
|
.phone-call-action-anchor {
|
|
height: 97px;
|
|
}
|
|
|
|
.phone-call-action::before {
|
|
width: 70px;
|
|
height: 70px;
|
|
}
|
|
|
|
.phone-call-action > svg {
|
|
width: 29px;
|
|
height: 29px;
|
|
}
|
|
|
|
.phone-call-action > span {
|
|
top: 79px;
|
|
}
|
|
|
|
.phone-in-call-keypad {
|
|
padding-bottom: 24px;
|
|
}
|
|
|
|
.phone-in-call-keypad__grid {
|
|
grid-template-columns: repeat(3, 64px);
|
|
gap: 10px 34px;
|
|
}
|
|
|
|
.phone-in-call-keypad__grid button {
|
|
width: 64px;
|
|
height: 64px;
|
|
}
|
|
|
|
.phone-in-call-keypad__footer {
|
|
grid-template-columns: repeat(3, 64px);
|
|
gap: 34px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.phone-in-call-keypad__end {
|
|
width: 64px !important;
|
|
height: 64px !important;
|
|
}
|
|
}
|
|
|
|
.phone-call-content {
|
|
min-height: 0;
|
|
}
|
|
|
|
.phone-bottom-tabbar {
|
|
--sky-app-accent: #007aff;
|
|
}
|
|
|
|
.phone-recents,
|
|
.phone-contacts,
|
|
.phone-contact-detail {
|
|
min-height: 100%;
|
|
padding: 8px 15px
|
|
calc(var(--sky-tabbar-height) + var(--sky-safe-area-bottom) + 16px);
|
|
}
|
|
|
|
.phone-contacts {
|
|
position: relative;
|
|
padding-top: 6px;
|
|
padding-right: 24px;
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
.phone-contacts-header {
|
|
padding-bottom: 16px;
|
|
}
|
|
|
|
.phone-contacts-toolbar {
|
|
display: grid;
|
|
grid-template-columns: 42px 1fr 42px;
|
|
align-items: center;
|
|
margin-bottom: 13px;
|
|
}
|
|
|
|
.phone-contacts-toolbar h1 {
|
|
margin: 0;
|
|
font-size: 17px;
|
|
text-align: center;
|
|
}
|
|
|
|
.phone-contacts-add {
|
|
display: flex;
|
|
width: 42px;
|
|
height: 42px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px solid rgba(142, 142, 147, 0.18);
|
|
border-radius: 50%;
|
|
background: rgba(118, 118, 128, 0.14);
|
|
color: inherit;
|
|
}
|
|
|
|
.phone-contacts-search {
|
|
display: flex;
|
|
min-height: 42px;
|
|
align-items: center;
|
|
gap: 9px;
|
|
padding: 0 14px;
|
|
border-radius: 16px;
|
|
background: rgba(118, 118, 128, 0.16);
|
|
color: #8e8e93;
|
|
}
|
|
|
|
.phone-contacts-search input {
|
|
width: 100%;
|
|
border: 0;
|
|
outline: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
font: inherit;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.phone-my-card {
|
|
display: flex;
|
|
width: 100%;
|
|
min-height: 72px;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 8px;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
font: inherit;
|
|
text-align: left;
|
|
}
|
|
|
|
.phone-my-card > span {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.phone-my-card strong {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.phone-my-card small {
|
|
margin-top: 2px;
|
|
color: #8e8e93;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.phone-contact-group {
|
|
scroll-margin-top: 10px;
|
|
}
|
|
|
|
.phone-contact-group h2 {
|
|
height: 28px;
|
|
margin: 0;
|
|
padding-top: 4px;
|
|
border-bottom: 1px solid rgba(142, 142, 147, 0.23);
|
|
color: #8e8e93;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.phone-contact-group--favorites h2 {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 5px;
|
|
color: #ffcc00;
|
|
}
|
|
|
|
.phone-contact-row {
|
|
display: grid;
|
|
grid-template-columns: 52px minmax(0, 1fr);
|
|
width: 100%;
|
|
min-height: 68px;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
font: inherit;
|
|
text-align: left;
|
|
}
|
|
|
|
.phone-contact-row:not(:last-child) .phone-contact-name {
|
|
border-bottom: 1px solid rgba(142, 142, 147, 0.2);
|
|
}
|
|
|
|
.phone-contact-name {
|
|
display: flex;
|
|
min-width: 0;
|
|
min-height: 68px;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
gap: 2px;
|
|
overflow: hidden;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-contact-name small {
|
|
color: #8e8e93;
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.phone-contact-index {
|
|
position: fixed;
|
|
z-index: 40;
|
|
top: 50%;
|
|
right: 5px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
pointer-events: auto;
|
|
transform: translateY(-42%);
|
|
}
|
|
|
|
.phone-contact-index button {
|
|
width: 24px;
|
|
height: 14px;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
color: #0a84ff;
|
|
font: inherit;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
line-height: 14px;
|
|
cursor: pointer;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
.phone-contact-index button svg {
|
|
display: block;
|
|
margin: auto;
|
|
}
|
|
|
|
.phone-contact-index button:active {
|
|
border-radius: 7px;
|
|
background: rgba(10, 132, 255, 0.2);
|
|
}
|
|
|
|
.phone-keypad {
|
|
position: relative;
|
|
display: flex;
|
|
min-height: 100%;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
padding: 56px 24px
|
|
calc(var(--sky-tabbar-height) + var(--sky-safe-area-bottom) + 16px);
|
|
}
|
|
|
|
.phone-keypad-number {
|
|
display: flex;
|
|
width: 100%;
|
|
min-height: 50px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 14px;
|
|
overflow: hidden;
|
|
font-size: 25px;
|
|
font-weight: 500;
|
|
letter-spacing: 0.4px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-keypad-suggestions {
|
|
position: absolute;
|
|
z-index: 5;
|
|
top: 18px;
|
|
right: 24px;
|
|
left: 24px;
|
|
display: grid;
|
|
width: auto;
|
|
max-height: 132px;
|
|
gap: 6px;
|
|
margin: 0 auto;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
scrollbar-width: none;
|
|
touch-action: pan-y;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.phone-keypad-suggestions::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.phone-keypad-suggestion {
|
|
display: grid;
|
|
min-height: 46px;
|
|
grid-template-columns: 36px minmax(0, 1fr);
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 5px 12px 5px 6px;
|
|
border: 1px solid rgba(255, 255, 255, 0.09);
|
|
border-radius: 18px;
|
|
color: #fff;
|
|
background: rgba(44, 44, 46, 0.82);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.07),
|
|
0 4px 12px rgba(0, 0, 0, 0.16);
|
|
font: inherit;
|
|
text-align: left;
|
|
backdrop-filter: blur(16px) saturate(140%);
|
|
-webkit-backdrop-filter: blur(16px) saturate(140%);
|
|
transition:
|
|
background-color 160ms ease-out,
|
|
border-color 160ms ease-out,
|
|
transform 160ms ease-out;
|
|
}
|
|
|
|
.phone-keypad-suggestion__avatar {
|
|
display: grid;
|
|
width: 36px;
|
|
height: 36px;
|
|
place-items: center;
|
|
overflow: hidden;
|
|
border-radius: 50%;
|
|
background: linear-gradient(145deg, #4b92d1, #195287);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.phone-keypad-suggestion__avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.phone-keypad-suggestion__copy {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.phone-keypad-suggestion__copy strong,
|
|
.phone-keypad-suggestion__copy small {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-keypad-suggestion__copy strong {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: color 160ms ease-out;
|
|
}
|
|
|
|
.phone-keypad-suggestion__copy small {
|
|
margin-top: 1px;
|
|
color: #a5a5aa;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.keypad-suggestions-enter-active,
|
|
.keypad-suggestions-leave-active {
|
|
transition:
|
|
opacity 180ms ease,
|
|
transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
.keypad-suggestions-enter-from,
|
|
.keypad-suggestions-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-8px);
|
|
}
|
|
|
|
.phone-keypad-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 78px);
|
|
gap: 18px 22px;
|
|
}
|
|
|
|
.phone-keypad-key {
|
|
display: flex;
|
|
width: 78px;
|
|
height: 78px;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 50%;
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(58, 58, 60, 0.92),
|
|
rgba(28, 28, 30, 0.88)
|
|
);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
|
0 5px 14px rgba(0, 0, 0, 0.24);
|
|
backdrop-filter: blur(18px) saturate(145%);
|
|
-webkit-backdrop-filter: blur(18px) saturate(145%);
|
|
color: white;
|
|
font: inherit;
|
|
transition:
|
|
background-color 120ms ease,
|
|
border-color 120ms ease,
|
|
box-shadow 120ms ease,
|
|
transform 120ms ease;
|
|
}
|
|
|
|
.phone-keypad-key:active {
|
|
background: #3a3a3c;
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.phone-keypad-key > span {
|
|
font-size: 36px;
|
|
font-weight: 400;
|
|
line-height: 34px;
|
|
}
|
|
|
|
.phone-keypad-key > small {
|
|
min-height: 13px;
|
|
margin-top: 3px;
|
|
font-size: 9px;
|
|
font-weight: 800;
|
|
letter-spacing: 2.4px;
|
|
line-height: 10px;
|
|
}
|
|
|
|
.phone-keypad-key > small.phone-keypad-plus {
|
|
font-size: 15px;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.phone-keypad-actions {
|
|
position: relative;
|
|
display: flex;
|
|
width: 100%;
|
|
min-height: 82px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.phone-keypad-call,
|
|
.phone-keypad-delete {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
color: white;
|
|
}
|
|
|
|
.phone-keypad-call {
|
|
width: 78px;
|
|
height: 78px;
|
|
border: 1px solid rgba(133, 235, 143, 0.78);
|
|
background: linear-gradient(180deg, #3e9a49, #2f7d39);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
|
0 5px 14px rgba(23, 101, 35, 0.3);
|
|
}
|
|
|
|
.phone-keypad-call:active {
|
|
background: #2d7336;
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.phone-keypad-delete {
|
|
position: absolute;
|
|
left: calc(50% + 62px);
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(58, 58, 60, 0.92),
|
|
rgba(28, 28, 30, 0.88)
|
|
);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
|
0 5px 14px rgba(0, 0, 0, 0.24);
|
|
backdrop-filter: blur(18px) saturate(145%);
|
|
-webkit-backdrop-filter: blur(18px) saturate(145%);
|
|
color: #d1d1d6;
|
|
}
|
|
|
|
.phone-keypad-delete:active {
|
|
background: #3a3a3c;
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
@media (max-height: 690px) {
|
|
.phone-keypad {
|
|
padding-top: 20px;
|
|
padding-bottom: calc(
|
|
var(--sky-tabbar-height) + var(--sky-safe-area-bottom) + 8px
|
|
);
|
|
}
|
|
|
|
.phone-keypad-suggestions {
|
|
top: 6px;
|
|
max-height: 104px;
|
|
}
|
|
|
|
.phone-keypad-grid {
|
|
grid-template-columns: repeat(3, 70px);
|
|
gap: 12px 18px;
|
|
}
|
|
|
|
.phone-keypad-key {
|
|
width: 70px;
|
|
height: 70px;
|
|
}
|
|
|
|
.phone-keypad-actions {
|
|
min-height: 70px;
|
|
margin-top: 14px;
|
|
}
|
|
|
|
.phone-keypad-suggestion:nth-child(n + 2) {
|
|
display: none;
|
|
}
|
|
|
|
.phone-keypad-call {
|
|
width: 68px;
|
|
height: 68px;
|
|
}
|
|
}
|
|
|
|
.phone-recents-header {
|
|
padding: 4px 0 14px;
|
|
}
|
|
|
|
.phone-recents-filter {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
width: 174px;
|
|
margin: 0 auto 14px;
|
|
padding: 3px;
|
|
border: 1px solid rgba(142, 142, 147, 0.25);
|
|
border-radius: 15px;
|
|
background: rgba(118, 118, 128, 0.14);
|
|
}
|
|
|
|
.phone-recents-filter button {
|
|
min-height: 30px;
|
|
border: 0;
|
|
border-radius: 12px;
|
|
background: transparent;
|
|
color: inherit;
|
|
font: inherit;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.phone-recents-filter button.active {
|
|
background: rgba(118, 118, 128, 0.34);
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.phone-recents-header h1 {
|
|
margin: 0 0 13px;
|
|
font-size: 30px;
|
|
line-height: 1.06;
|
|
letter-spacing: -0.8px;
|
|
}
|
|
|
|
.phone-recents-search {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
min-height: 42px;
|
|
padding: 0 14px;
|
|
border-radius: 16px;
|
|
background: rgba(118, 118, 128, 0.16);
|
|
color: #8e8e93;
|
|
}
|
|
|
|
.phone-recents-search input {
|
|
width: 100%;
|
|
border: 0;
|
|
outline: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
font: inherit;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.phone-recents-list {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.phone-recent-row {
|
|
display: grid;
|
|
grid-template-columns: 52px minmax(0, 1fr) auto 38px;
|
|
align-items: center;
|
|
min-height: 76px;
|
|
column-gap: 10px;
|
|
}
|
|
|
|
.phone-recent-row:not(:last-child) {
|
|
border-bottom: 1px solid rgba(142, 142, 147, 0.2);
|
|
}
|
|
|
|
.phone-contact-avatar {
|
|
display: flex;
|
|
width: 52px;
|
|
height: 52px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
border-radius: 50%;
|
|
background: linear-gradient(145deg, #5b91c2, #22527d);
|
|
color: white;
|
|
font-size: 19px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.phone-contact-avatar img,
|
|
.phone-contact-editor__avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.phone-recent-call {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
align-self: stretch;
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
text-align: left;
|
|
}
|
|
|
|
.phone-recent-name {
|
|
overflow: hidden;
|
|
max-width: 100%;
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-recent-name--missed {
|
|
color: #ff453a;
|
|
}
|
|
|
|
.phone-recent-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
overflow: hidden;
|
|
max-width: 100%;
|
|
margin-top: 2px;
|
|
color: #8e8e93;
|
|
font-size: 13px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-recent-date {
|
|
color: #8e8e93;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-recent-info {
|
|
display: flex;
|
|
width: 36px;
|
|
height: 36px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
background: rgba(118, 118, 128, 0.14);
|
|
color: #0a84ff;
|
|
}
|
|
|
|
.phone-detail-header {
|
|
display: flex;
|
|
min-height: 38px;
|
|
align-items: center;
|
|
}
|
|
|
|
.phone-detail-back {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
color: #0a84ff;
|
|
font: inherit;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.phone-contact-hero {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 6px 0 22px;
|
|
text-align: center;
|
|
}
|
|
|
|
.phone-contact-avatar--large {
|
|
width: 92px;
|
|
height: 92px;
|
|
font-size: 32px;
|
|
}
|
|
|
|
.phone-contact-hero h2 {
|
|
max-width: 100%;
|
|
margin: 12px 0 2px;
|
|
overflow: hidden;
|
|
font-size: 24px;
|
|
letter-spacing: -0.45px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-contact-hero p {
|
|
margin: 0;
|
|
color: #8e8e93;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.phone-contact-hero .phone-contact-organization {
|
|
margin-top: 2px;
|
|
color: rgba(255, 255, 255, 0.72);
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.phone-contact-actions {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 10px;
|
|
width: 100%;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.phone-contact-actions button {
|
|
display: flex;
|
|
min-width: 0;
|
|
min-height: 70px;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
border: 0;
|
|
border-radius: 14px;
|
|
background: rgba(118, 118, 128, 0.15);
|
|
color: #0a84ff;
|
|
font: inherit;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.phone-contact-actions button > span {
|
|
display: flex;
|
|
min-height: 25px;
|
|
align-items: center;
|
|
}
|
|
|
|
.phone-own-profile-card {
|
|
overflow: hidden;
|
|
border-radius: 16px;
|
|
background: rgba(118, 118, 128, 0.12);
|
|
}
|
|
|
|
.phone-own-profile-card > div {
|
|
display: grid;
|
|
grid-template-columns: 32px minmax(0, 1fr);
|
|
min-height: 62px;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-left: 14px;
|
|
padding-right: 14px;
|
|
}
|
|
|
|
.phone-own-profile-card > div:not(:last-child) {
|
|
border-bottom: 1px solid rgba(142, 142, 147, 0.2);
|
|
}
|
|
|
|
.phone-own-profile-card > div > span {
|
|
display: flex;
|
|
width: 30px;
|
|
height: 30px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
background: #0a84ff;
|
|
color: white;
|
|
}
|
|
|
|
.phone-own-profile-card p {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
margin: 0;
|
|
}
|
|
|
|
.phone-own-profile-card small {
|
|
color: #8e8e93;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.phone-own-profile-card strong {
|
|
overflow: hidden;
|
|
margin-top: 2px;
|
|
font-size: 14px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-history-card {
|
|
overflow: hidden;
|
|
border-radius: 16px;
|
|
background: rgba(118, 118, 128, 0.12);
|
|
}
|
|
|
|
.phone-history-card h3 {
|
|
margin: 0;
|
|
padding: 13px 14px 10px;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.phone-history-row {
|
|
display: grid;
|
|
grid-template-columns: 26px minmax(0, 1fr) auto;
|
|
align-items: center;
|
|
min-height: 58px;
|
|
gap: 8px;
|
|
margin-left: 14px;
|
|
padding-right: 14px;
|
|
border-top: 1px solid rgba(142, 142, 147, 0.2);
|
|
}
|
|
|
|
.phone-history-direction {
|
|
color: #0a84ff;
|
|
}
|
|
|
|
.phone-history-direction--missed {
|
|
color: #ff453a;
|
|
}
|
|
|
|
.phone-history-copy {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.phone-history-copy strong {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.phone-history-copy small,
|
|
.phone-history-row time,
|
|
.phone-history-empty {
|
|
color: #8e8e93;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.phone-history-row time {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-history-empty {
|
|
margin: 0;
|
|
padding: 18px 14px;
|
|
text-align: center;
|
|
}
|
|
|
|
.phone-call-content--profile {
|
|
background: transparent;
|
|
color: #fff;
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
.phone-calls-app--profile {
|
|
background: linear-gradient(
|
|
180deg,
|
|
#a8b3b7 0,
|
|
#8991a2 18%,
|
|
#172f50 55%,
|
|
#061426 100%
|
|
) !important;
|
|
color: #fff;
|
|
}
|
|
|
|
.phone-contact-detail {
|
|
position: relative;
|
|
padding: 0 15px 112px;
|
|
}
|
|
|
|
.phone-detail-header {
|
|
position: sticky;
|
|
z-index: 12;
|
|
top: 0;
|
|
display: grid;
|
|
height: 58px;
|
|
grid-template-columns: 48px minmax(0, 1fr) minmax(48px, auto);
|
|
align-items: start;
|
|
gap: 8px;
|
|
margin: 0 -15px;
|
|
padding: 7px 15px;
|
|
overflow: visible;
|
|
}
|
|
|
|
.phone-detail-header-button,
|
|
.phone-detail-edit {
|
|
min-width: 0;
|
|
height: 44px;
|
|
padding: 0;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
color: #fff;
|
|
background: rgba(21, 35, 45, 0.88);
|
|
box-shadow: 0 2px 9px rgba(7, 12, 20, 0.16);
|
|
}
|
|
|
|
.phone-detail-header-button {
|
|
width: 44px;
|
|
}
|
|
|
|
.phone-detail-edit {
|
|
justify-self: end;
|
|
width: auto;
|
|
min-width: 86px;
|
|
padding: 0 17px;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.phone-detail-edit-spacer {
|
|
width: 44px;
|
|
}
|
|
|
|
.phone-detail-header-spacer {
|
|
min-width: 0;
|
|
}
|
|
|
|
.phone-contact-hero {
|
|
padding: 31px 0 19px;
|
|
}
|
|
|
|
.phone-contact-avatar--large {
|
|
width: 148px;
|
|
height: 148px;
|
|
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
background: linear-gradient(
|
|
155deg,
|
|
rgba(176, 188, 195, 0.74),
|
|
rgba(45, 105, 158, 0.66)
|
|
);
|
|
box-shadow:
|
|
inset 0 1px 2px rgba(255, 255, 255, 0.38),
|
|
0 2px 3px rgba(18, 15, 43, 0.25);
|
|
font-size: 64px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.phone-contact-hero h2 {
|
|
margin: 14px 0 0;
|
|
color: #fff;
|
|
font-size: 29px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.65px;
|
|
}
|
|
|
|
.phone-contact-actions {
|
|
display: grid;
|
|
width: 100%;
|
|
grid-template-columns: repeat(4, 62px);
|
|
justify-content: center;
|
|
gap: 12px;
|
|
margin-top: 22px;
|
|
}
|
|
|
|
.phone-profile-action {
|
|
width: 62px;
|
|
min-width: 62px;
|
|
height: 62px;
|
|
min-height: 62px;
|
|
padding: 0;
|
|
border: 1px solid rgba(10, 132, 255, 0.58);
|
|
border-radius: 50%;
|
|
color: #fff;
|
|
background: rgba(18, 82, 145, 0.72);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
|
0 4px 14px rgba(13, 9, 33, 0.18);
|
|
}
|
|
|
|
.phone-profile-action:disabled {
|
|
color: rgba(255, 255, 255, 0.32);
|
|
opacity: 1;
|
|
}
|
|
|
|
.phone-profile-content {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.phone-profile-card,
|
|
.phone-own-profile-card,
|
|
.phone-history-card {
|
|
overflow: hidden;
|
|
border: 1px solid rgba(255, 255, 255, 0.035);
|
|
border-radius: 24px;
|
|
color: #fff;
|
|
background: rgba(18, 43, 69, 0.8);
|
|
box-shadow: 0 8px 22px rgba(5, 4, 20, 0.12);
|
|
backdrop-filter: blur(20px) saturate(125%);
|
|
-webkit-backdrop-filter: blur(20px) saturate(125%);
|
|
}
|
|
|
|
.phone-profile-info-card > button,
|
|
.phone-profile-options-card > button,
|
|
.phone-profile-single-option > button {
|
|
display: flex;
|
|
width: calc(100% - 30px);
|
|
min-height: 66px;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
margin: 0 15px;
|
|
padding: 0;
|
|
border: 0;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.17);
|
|
color: inherit;
|
|
background: transparent;
|
|
font: inherit;
|
|
text-align: left;
|
|
}
|
|
|
|
.phone-profile-info-card > button:last-of-type,
|
|
.phone-profile-options-card > button:last-child,
|
|
.phone-profile-single-option > button:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.phone-profile-info-card > button > span {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.phone-profile-info-card small {
|
|
color: #64a8ff;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.phone-profile-info-card strong {
|
|
margin-top: 2px;
|
|
overflow: hidden;
|
|
font-size: 15px;
|
|
font-weight: 400;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.phone-profile-info-card > button > svg {
|
|
flex: none;
|
|
color: #0a84ff;
|
|
transition:
|
|
color 160ms ease-out,
|
|
filter 160ms ease-out,
|
|
transform 160ms ease-out;
|
|
}
|
|
|
|
.phone-profile-notes {
|
|
min-height: 105px;
|
|
margin: 0 15px;
|
|
padding: 17px 0;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.17);
|
|
font-size: 15px;
|
|
}
|
|
|
|
.phone-profile-notes > span {
|
|
display: block;
|
|
color: #fff;
|
|
}
|
|
|
|
.phone-profile-notes > p {
|
|
margin: 8px 0 0;
|
|
color: rgba(255, 255, 255, 0.72);
|
|
line-height: 1.4;
|
|
overflow-wrap: anywhere;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.phone-profile-options-card > button,
|
|
.phone-profile-single-option > button {
|
|
min-height: 54px;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.phone-profile-options-card > button > span,
|
|
.phone-profile-single-option > button > span {
|
|
transition:
|
|
color 160ms ease-out,
|
|
transform 160ms ease-out;
|
|
}
|
|
|
|
.phone-profile-options-card > button:disabled,
|
|
.phone-profile-single-option > button:disabled {
|
|
color: inherit;
|
|
opacity: 1;
|
|
}
|
|
|
|
.phone-history-card h3 {
|
|
padding: 15px 16px 11px;
|
|
color: #fff;
|
|
}
|
|
|
|
.phone-history-row {
|
|
margin-left: 15px;
|
|
padding-right: 15px;
|
|
border-top-color: rgba(255, 255, 255, 0.17);
|
|
}
|
|
|
|
.phone-history-copy small,
|
|
.phone-history-row time,
|
|
.phone-history-empty {
|
|
color: rgba(255, 255, 255, 0.62);
|
|
}
|
|
|
|
.phone-contact-editor-sheet :deep(.sky-sheet__panel) {
|
|
width: 100%;
|
|
height: calc(100% - 52px);
|
|
max-height: calc(100% - 52px);
|
|
border-radius: 30px 30px 0 0;
|
|
background: #3a3a3c;
|
|
box-shadow: 0 -10px 35px rgba(0, 0, 0, 0.28);
|
|
}
|
|
|
|
.phone-contact-editor {
|
|
position: relative;
|
|
display: flex;
|
|
height: 100%;
|
|
min-height: 0;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
border-radius: inherit;
|
|
color: #fff;
|
|
background: #3a3a3c;
|
|
}
|
|
|
|
.phone-contact-editor::before {
|
|
position: absolute;
|
|
z-index: 3;
|
|
top: 7px;
|
|
left: 50%;
|
|
width: 44px;
|
|
height: 5px;
|
|
border-radius: 999px;
|
|
background: rgba(255, 255, 255, 0.38);
|
|
content: '';
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.phone-contact-editor__header {
|
|
position: relative;
|
|
z-index: 2;
|
|
display: grid;
|
|
height: 78px;
|
|
padding: 18px 18px 6px;
|
|
grid-template-columns: 52px 1fr 52px;
|
|
align-items: center;
|
|
flex: 0 0 78px;
|
|
background: linear-gradient(180deg, #3a3a3c 72%, rgba(58, 58, 60, 0));
|
|
}
|
|
|
|
.phone-contact-editor__header h2 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
}
|
|
|
|
.phone-contact-editor__header-button {
|
|
width: 46px;
|
|
min-width: 46px;
|
|
height: 46px;
|
|
min-height: 46px;
|
|
padding: 0;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
color: #fff;
|
|
background: rgba(28, 28, 30, 0.58);
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.phone-contact-editor__header-button:last-child {
|
|
justify-self: end;
|
|
background: rgba(72, 72, 74, 0.76);
|
|
}
|
|
|
|
.phone-contact-editor__scroll {
|
|
min-height: 0;
|
|
flex: 1;
|
|
padding: 2px 14px 42px;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
scrollbar-width: none;
|
|
touch-action: pan-y;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.phone-contact-editor__scroll::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.phone-contact-editor__avatar {
|
|
display: grid;
|
|
width: 154px;
|
|
height: 154px;
|
|
margin: 0;
|
|
padding: 0;
|
|
place-items: center;
|
|
border: 1px solid rgba(255, 255, 255, 0.11);
|
|
border-radius: 50%;
|
|
color: #fff;
|
|
background: linear-gradient(155deg, #4b92d1 0%, #195287 100%);
|
|
box-shadow:
|
|
inset 0 1px 1px rgba(255, 255, 255, 0.16),
|
|
0 8px 18px rgba(5, 42, 78, 0.24);
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.phone-contact-editor__avatar-wrap {
|
|
position: relative;
|
|
width: 154px;
|
|
height: 154px;
|
|
margin: 4px auto 12px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.phone-contact-editor__remove-photo {
|
|
position: absolute !important;
|
|
z-index: 2;
|
|
inset: 0;
|
|
display: flex !important;
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
flex-direction: column !important;
|
|
gap: 7px !important;
|
|
padding: 0 !important;
|
|
border: 0 !important;
|
|
border-radius: 50% !important;
|
|
color: #fff !important;
|
|
background: rgba(7, 35, 62, 0.7) !important;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transform: scale(0.96);
|
|
transition:
|
|
opacity 170ms ease,
|
|
transform 170ms ease !important;
|
|
}
|
|
|
|
.phone-contact-editor__remove-photo > span {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.phone-contact-editor__avatar-wrap--has-photo:hover
|
|
.phone-contact-editor__remove-photo,
|
|
.phone-contact-editor__remove-photo:focus-visible {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
transform: scale(1);
|
|
}
|
|
|
|
.phone-contact-editor__avatar img {
|
|
transition:
|
|
filter 170ms ease,
|
|
transform 170ms ease;
|
|
}
|
|
|
|
.phone-contact-editor__avatar-wrap--has-photo:hover
|
|
.phone-contact-editor__avatar
|
|
img {
|
|
filter: brightness(0.58);
|
|
transform: scale(1.015);
|
|
}
|
|
|
|
.phone-contact-editor__avatar span {
|
|
font-size: 53px;
|
|
font-weight: 600;
|
|
letter-spacing: -2px;
|
|
}
|
|
|
|
.phone-contact-editor__photo {
|
|
margin-bottom: 22px;
|
|
text-align: center;
|
|
}
|
|
|
|
.phone-contact-editor__photo-actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.phone-contact-editor__photo-actions :deep(button) {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
min-height: 34px;
|
|
padding-inline: 16px;
|
|
color: #0a84ff;
|
|
}
|
|
|
|
.phone-contact-editor__name-list,
|
|
.phone-contact-editor__number-list,
|
|
.phone-contact-editor__notes-list {
|
|
margin: 0 0 16px;
|
|
color: #fff;
|
|
}
|
|
|
|
.phone-contact-editor__name-list :deep(ul),
|
|
.phone-contact-editor__number-list :deep(ul),
|
|
.phone-contact-editor__notes-list :deep(ul) {
|
|
overflow: hidden;
|
|
border-radius: 24px;
|
|
background: #2c2c2e;
|
|
}
|
|
|
|
.phone-contact-editor__name-list :deep(li),
|
|
.phone-contact-editor__number-list :deep(li),
|
|
.phone-contact-editor__notes-list :deep(li) {
|
|
min-height: 58px;
|
|
background: transparent;
|
|
}
|
|
|
|
.phone-contact-editor__name-list :deep(input),
|
|
.phone-contact-editor__number-list :deep(input),
|
|
.phone-contact-editor__notes-list :deep(textarea) {
|
|
color: #fff;
|
|
font-size: 17px;
|
|
}
|
|
|
|
.phone-contact-editor__name-list :deep(input::placeholder),
|
|
.phone-contact-editor__number-list :deep(input::placeholder),
|
|
.phone-contact-editor__notes-list :deep(textarea::placeholder) {
|
|
color: #a7a7ac;
|
|
opacity: 1;
|
|
}
|
|
|
|
.phone-contact-editor__notes-list :deep(textarea) {
|
|
min-height: 112px;
|
|
resize: none;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.phone-contact-editor__add-icon {
|
|
display: grid;
|
|
width: 25px;
|
|
height: 25px;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
color: #fff;
|
|
background: #30d158;
|
|
}
|
|
|
|
.phone-contact-editor__error {
|
|
margin: -4px 14px 16px;
|
|
color: #ff6961;
|
|
font-size: 13px;
|
|
text-align: center;
|
|
}
|
|
|
|
.phone-contact-editor__delete {
|
|
width: 100%;
|
|
min-height: 56px;
|
|
border-radius: 22px;
|
|
color: #ff453a;
|
|
background: #2c2c2e;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.phone-contact-editor--light {
|
|
color: #111;
|
|
background: #e5e5ea;
|
|
}
|
|
|
|
.phone-contact-editor--light .phone-contact-editor__header {
|
|
background: linear-gradient(180deg, #e5e5ea 72%, rgba(229, 229, 234, 0));
|
|
}
|
|
|
|
.phone-contact-editor--light .phone-contact-editor__header-button {
|
|
border-color: rgba(60, 60, 67, 0.18);
|
|
color: #111;
|
|
background: rgba(255, 255, 255, 0.62);
|
|
}
|
|
|
|
.phone-contact-editor--light .phone-contact-editor__name-list,
|
|
.phone-contact-editor--light .phone-contact-editor__number-list,
|
|
.phone-contact-editor--light .phone-contact-editor__notes-list {
|
|
color: #111;
|
|
}
|
|
|
|
.phone-contact-editor--light .phone-contact-editor__name-list :deep(ul),
|
|
.phone-contact-editor--light .phone-contact-editor__number-list :deep(ul),
|
|
.phone-contact-editor--light .phone-contact-editor__notes-list :deep(ul),
|
|
.phone-contact-editor--light .phone-contact-editor__delete {
|
|
background: rgba(255, 255, 255, 0.72);
|
|
}
|
|
|
|
.phone-contact-editor--light .phone-contact-editor__name-list :deep(input),
|
|
.phone-contact-editor--light .phone-contact-editor__number-list :deep(input),
|
|
.phone-contact-editor--light .phone-contact-editor__notes-list :deep(textarea) {
|
|
color: #111;
|
|
}
|
|
|
|
@media (max-width: 360px) {
|
|
.phone-detail-header {
|
|
grid-template-columns: 44px minmax(0, 1fr) minmax(44px, auto);
|
|
gap: 5px;
|
|
}
|
|
|
|
.phone-detail-edit {
|
|
min-width: 74px;
|
|
padding: 0 12px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.phone-contact-actions {
|
|
grid-template-columns: repeat(4, 56px);
|
|
gap: 9px;
|
|
}
|
|
|
|
.phone-profile-action {
|
|
width: 56px;
|
|
min-width: 56px;
|
|
height: 56px;
|
|
min-height: 56px;
|
|
}
|
|
}
|
|
|
|
.phone-calls-app button:not(:disabled),
|
|
.phone-contact-editor button:not(:disabled) {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.phone-recents-filter button,
|
|
.phone-recents-search,
|
|
.phone-contacts-search,
|
|
.phone-recent-row,
|
|
.phone-recent-info,
|
|
.phone-contacts-add,
|
|
.phone-my-card,
|
|
.phone-contact-row,
|
|
.phone-contact-index button,
|
|
.phone-keypad-call,
|
|
.phone-profile-action,
|
|
.phone-detail-header-button,
|
|
.phone-detail-edit,
|
|
.phone-profile-info-card > button,
|
|
.phone-profile-options-card > button,
|
|
.phone-profile-single-option > button,
|
|
.phone-call-more__item,
|
|
.phone-contact-editor__avatar,
|
|
.phone-contact-editor__header-button,
|
|
.phone-contact-editor__delete,
|
|
.phone-contact-editor__photo-actions :deep(button) {
|
|
transition:
|
|
color 160ms ease-out,
|
|
background-color 160ms ease-out,
|
|
border-color 160ms ease-out,
|
|
box-shadow 160ms ease-out,
|
|
filter 160ms ease-out,
|
|
transform 160ms ease-out;
|
|
}
|
|
|
|
.phone-calls-app button:focus-visible,
|
|
.phone-contact-editor button:focus-visible {
|
|
outline: 2px solid #64a8ff;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.phone-keypad-key:hover,
|
|
.phone-keypad-delete:hover {
|
|
border-color: rgba(255, 255, 255, 0.14);
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(68, 68, 70, 0.94),
|
|
rgba(38, 38, 40, 0.9)
|
|
);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
|
0 3px 9px rgba(0, 0, 0, 0.2);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.phone-keypad-call:hover {
|
|
border-color: rgba(151, 245, 161, 0.9);
|
|
background: linear-gradient(180deg, #48aa54, #378c42);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.26),
|
|
0 6px 16px rgba(52, 199, 89, 0.3);
|
|
transform: translateY(-2px) scale(1.015);
|
|
}
|
|
|
|
.phone-in-call-keypad__grid button:hover {
|
|
border-color: rgba(255, 255, 255, 0.22);
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(255, 255, 255, 0.2),
|
|
rgba(255, 255, 255, 0.12)
|
|
);
|
|
box-shadow: 0 3px 9px rgba(10, 30, 80, 0.15);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
@media (hover: hover) and (pointer: fine) {
|
|
.phone-keypad-suggestion:hover .phone-keypad-suggestion__copy strong {
|
|
color: #68adff;
|
|
}
|
|
|
|
.phone-recents-filter button:not(.active):hover {
|
|
background: rgba(118, 118, 128, 0.22);
|
|
}
|
|
|
|
.phone-recents-search:hover,
|
|
.phone-contacts-search:hover {
|
|
background: rgba(118, 118, 128, 0.24);
|
|
box-shadow: inset 0 0 0 1px rgba(142, 142, 147, 0.18);
|
|
}
|
|
|
|
.phone-recent-row:hover,
|
|
.phone-my-card:hover,
|
|
.phone-contact-row:hover {
|
|
border-radius: 14px;
|
|
background: rgba(118, 118, 128, 0.12);
|
|
box-shadow: inset 0 0 0 1px rgba(142, 142, 147, 0.08);
|
|
}
|
|
|
|
.phone-recent-info:hover,
|
|
.phone-contacts-add:hover,
|
|
.phone-contact-index button:hover {
|
|
background: rgba(10, 132, 255, 0.16);
|
|
color: #55aaff;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.phone-call-action:not(:disabled):not(.phone-call-action--end):not(
|
|
.phone-call-action--answer
|
|
):hover::before {
|
|
background: rgba(170, 187, 231, 0.32);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
|
0 4px 12px rgba(11, 23, 62, 0.16);
|
|
transform: translateX(-50%) translateY(-1px) scale(1.012);
|
|
}
|
|
|
|
.phone-call-action--end:not(:disabled):hover::before {
|
|
border-color: rgba(255, 190, 185, 0.92);
|
|
background: #ff3b30;
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
|
0 5px 14px rgba(215, 45, 34, 0.34);
|
|
transform: translateX(-50%) translateY(-1px) scale(1.012);
|
|
}
|
|
|
|
.phone-call-action--answer:not(:disabled):hover::before {
|
|
border-color: rgba(190, 255, 198, 0.9);
|
|
background: #3bc45d;
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
|
0 5px 14px rgba(52, 168, 83, 0.3);
|
|
transform: translateX(-50%) translateY(-1px) scale(1.012);
|
|
}
|
|
|
|
.phone-call-more__item:not(:disabled):hover {
|
|
border-color: rgba(140, 165, 229, 0.66) !important;
|
|
background: rgba(49, 57, 84, 0.68) !important;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.phone-detail-header-button:not(:disabled):hover,
|
|
.phone-detail-edit:not(:disabled):hover {
|
|
border-color: rgba(255, 255, 255, 0.22);
|
|
background: rgba(37, 54, 66, 0.96);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.phone-profile-action:not(:disabled):hover {
|
|
border-color: rgba(100, 168, 255, 0.7);
|
|
background: rgba(22, 92, 158, 0.84);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
|
0 4px 12px rgba(5, 42, 78, 0.2);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.phone-profile-options-card > button:not(:disabled):hover > span,
|
|
.phone-profile-single-option > button:not(:disabled):hover > span {
|
|
color: #64a8ff;
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.phone-profile-info-card > button:not(:disabled):hover > svg {
|
|
color: #64a8ff;
|
|
filter: brightness(1.08);
|
|
transform: translateX(1px);
|
|
}
|
|
|
|
.phone-contact-editor__avatar-wrap:not(
|
|
.phone-contact-editor__avatar-wrap--has-photo
|
|
):hover
|
|
.phone-contact-editor__avatar {
|
|
border-color: rgba(255, 255, 255, 0.22);
|
|
filter: brightness(1.06);
|
|
transform: scale(1.012);
|
|
}
|
|
|
|
.phone-contact-editor__header-button:not(:disabled):hover,
|
|
.phone-contact-editor__photo-actions :deep(button:not(:disabled):hover) {
|
|
filter: brightness(1.08);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.phone-contact-editor__delete:not(:disabled):hover {
|
|
background: #38383a;
|
|
}
|
|
|
|
.phone-calls-app :deep([data-active-tab] button[aria-pressed='false']:hover) {
|
|
background: rgba(118, 118, 128, 0.14);
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
|
|
@media (hover: none) {
|
|
.phone-contact-editor__remove-photo {
|
|
inset: auto 5px 5px auto;
|
|
width: 44px !important;
|
|
height: 44px !important;
|
|
background: rgba(7, 35, 62, 0.86) !important;
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
transform: none;
|
|
}
|
|
|
|
.phone-contact-editor__remove-photo > span {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.phone-detail-header,
|
|
.phone-calls-app button,
|
|
.phone-contact-editor button {
|
|
transition-duration: 1ms;
|
|
transition-delay: 0ms;
|
|
}
|
|
}
|
|
|
|
.phone-app--light .phone-recents-filter,
|
|
.phone-app--light .phone-recents-search,
|
|
.phone-app--light .phone-contacts-add,
|
|
.phone-app--light .phone-contacts-search,
|
|
.phone-app--light .phone-recent-info,
|
|
.phone-app--light .phone-contact-actions button,
|
|
.phone-app--light .phone-own-profile-card,
|
|
.phone-app--light .phone-history-card {
|
|
background: rgba(118, 118, 128, 0.1);
|
|
}
|
|
|
|
.phone-app--light .phone-keypad-suggestion {
|
|
border-color: rgba(60, 60, 67, 0.1);
|
|
color: #111;
|
|
background: rgba(229, 229, 234, 0.88);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.72),
|
|
0 4px 12px rgba(60, 60, 67, 0.12);
|
|
}
|
|
|
|
.phone-app--light .phone-keypad-suggestion__copy small {
|
|
color: #6e6e73;
|
|
}
|
|
|
|
.phone-app--light .phone-keypad-key,
|
|
.phone-app--light .phone-keypad-delete {
|
|
border-color: rgba(60, 60, 67, 0.1);
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(255, 255, 255, 0.9),
|
|
rgba(229, 229, 234, 0.88)
|
|
);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.72),
|
|
0 5px 14px rgba(60, 60, 67, 0.14);
|
|
color: #000;
|
|
}
|
|
|
|
.phone-app--light .phone-keypad-key:active,
|
|
.phone-app--light .phone-keypad-delete:active {
|
|
background: #d1d1d6;
|
|
}
|
|
|
|
.phone-app--light .phone-keypad-key:hover,
|
|
.phone-app--light .phone-keypad-delete:hover {
|
|
border-color: rgba(60, 60, 67, 0.12);
|
|
background: linear-gradient(
|
|
180deg,
|
|
rgba(248, 248, 250, 0.94),
|
|
rgba(222, 222, 227, 0.9)
|
|
);
|
|
}
|
|
</style>
|