mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-02 03:08:54 +00:00
ADD - share contacts and rich content in chats
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { kButton } from 'konsta/vue'
|
||||
import {
|
||||
Check,
|
||||
ChevronRight,
|
||||
MessageCircle,
|
||||
UserRound,
|
||||
UserPlus,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type { SmsSharedContact } from '@/types/messages'
|
||||
|
||||
const props = defineProps<{
|
||||
addLabel: string
|
||||
contact: SmsSharedContact
|
||||
messageLabel: string
|
||||
saved: boolean
|
||||
savedLabel: string
|
||||
}>()
|
||||
const emit = defineEmits<{ message: []; save: [] }>()
|
||||
const imageFailed = ref(false)
|
||||
watch(
|
||||
() => props.contact.avatar_url,
|
||||
() => {
|
||||
imageFailed.value = false
|
||||
},
|
||||
)
|
||||
const displayName = computed(
|
||||
() => props.contact.name.trim() || props.contact.phone_number,
|
||||
)
|
||||
const showNumber = computed(() => Boolean(props.contact.name.trim()))
|
||||
const initials = computed(() =>
|
||||
displayName.value
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]?.toUpperCase())
|
||||
.join(''),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="message-contact-card">
|
||||
<div class="message-contact-card__identity">
|
||||
<span class="message-contact-card__avatar">
|
||||
<img
|
||||
v-if="contact.avatar_url && !imageFailed"
|
||||
:src="contact.avatar_url"
|
||||
alt=""
|
||||
@error="imageFailed = true"
|
||||
/>
|
||||
<b v-else-if="initials">{{ initials }}</b>
|
||||
<UserRound v-else :size="28" />
|
||||
</span>
|
||||
<div>
|
||||
<strong>{{ displayName }}</strong>
|
||||
<small v-if="contact.organization">{{ contact.organization }}</small>
|
||||
<small v-if="showNumber">({{ contact.phone_number }})</small>
|
||||
</div>
|
||||
<ChevronRight :size="20" class="message-contact-card__chevron" />
|
||||
</div>
|
||||
<div class="message-contact-card__actions">
|
||||
<k-button rounded tonal @click.stop="emit('message')">
|
||||
<MessageCircle :size="17" />
|
||||
{{ messageLabel }}
|
||||
</k-button>
|
||||
<k-button
|
||||
rounded
|
||||
tonal
|
||||
:disabled="saved"
|
||||
@click.stop="emit('save')"
|
||||
>
|
||||
<Check v-if="saved" :size="17" />
|
||||
<UserPlus v-else :size="17" />
|
||||
{{ saved ? savedLabel : addLabel }}
|
||||
</k-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message-contact-card {
|
||||
width: min(258px, 72cqw);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(255 255 255 / 48%);
|
||||
border-radius: 21px;
|
||||
color: #151517;
|
||||
background: linear-gradient(155deg, rgb(255 255 255 / 96%), #edf5ff);
|
||||
box-shadow: 0 8px 24px rgb(22 63 112 / 14%);
|
||||
}
|
||||
|
||||
.message-contact-card__identity {
|
||||
display: grid;
|
||||
grid-template-columns: 58px minmax(0, 1fr) 20px;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
min-height: 82px;
|
||||
padding: 13px 12px;
|
||||
}
|
||||
|
||||
.message-contact-card__avatar {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
background: linear-gradient(145deg, #64d2ff, #0a84ff);
|
||||
box-shadow: 0 4px 14px rgb(10 132 255 / 24%);
|
||||
}
|
||||
|
||||
.message-contact-card__avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.message-contact-card__identity b {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.message-contact-card__identity div,
|
||||
.message-contact-card__identity strong,
|
||||
.message-contact-card__identity small {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-contact-card__identity strong,
|
||||
.message-contact-card__identity small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-contact-card__identity strong {
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.message-contact-card__identity small {
|
||||
margin-top: 3px;
|
||||
color: #6e6e73;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.message-contact-card__chevron {
|
||||
color: #8e8e93;
|
||||
}
|
||||
|
||||
.message-contact-card__actions {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
padding: 8px;
|
||||
border-top: 1px solid rgb(60 60 67 / 12%);
|
||||
background: rgb(255 255 255 / 58%);
|
||||
}
|
||||
|
||||
.message-contact-card__actions :deep(.button) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
min-height: 36px;
|
||||
justify-content: flex-start;
|
||||
padding-inline: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:global(.phone-app.dark) .message-contact-card {
|
||||
color: #f7f7f7;
|
||||
border-color: rgb(255 255 255 / 10%);
|
||||
background: linear-gradient(155deg, #34363b, #242a33);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 24%);
|
||||
}
|
||||
|
||||
:global(.phone-app.dark) .message-contact-card__identity small {
|
||||
color: #aeaeb2;
|
||||
}
|
||||
|
||||
:global(.phone-app.dark) .message-contact-card__actions {
|
||||
border-top-color: rgb(255 255 255 / 10%);
|
||||
background: rgb(0 0 0 / 10%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,257 @@
|
||||
<script setup lang="ts">
|
||||
import { Image, MapPin, Music2, Play, UserRound } from 'lucide-vue-next'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { getPhoneApp } from '@/config/apps'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import type { EasySharePayload } from '@/types/easyshare'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
compact?: boolean
|
||||
payload: EasySharePayload
|
||||
variant?: 'darkchat' | 'flare' | 'messages'
|
||||
}>(),
|
||||
{ compact: false, variant: 'messages' },
|
||||
)
|
||||
const phone = usePhoneStore()
|
||||
const imageFailed = ref(false)
|
||||
const sourceApp = computed(() => getPhoneApp(props.payload.appId))
|
||||
const isVideo = computed(
|
||||
() =>
|
||||
props.payload.kind === 'video' ||
|
||||
(props.payload.appId === 'fliptok' && props.payload.kind === 'post'),
|
||||
)
|
||||
const fallbackIcon = computed(() => {
|
||||
if (props.payload.kind === 'location') return MapPin
|
||||
if (props.payload.kind === 'profile') return UserRound
|
||||
if (props.payload.kind === 'track' || props.payload.kind === 'playlist') {
|
||||
return Music2
|
||||
}
|
||||
if (props.payload.kind === 'video') return Play
|
||||
return Image
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.payload.imageUrl,
|
||||
() => {
|
||||
imageFailed.value = false
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="shared-content-card"
|
||||
:class="[
|
||||
`shared-content-card--${variant}`,
|
||||
{ 'shared-content-card--compact': compact },
|
||||
]"
|
||||
>
|
||||
<div class="shared-content-card__source">
|
||||
<img v-if="sourceApp?.iconImage" :src="sourceApp.iconImage" alt="" />
|
||||
<span>
|
||||
<b>{{ sourceApp ? phone.t(sourceApp.labelKey) : payload.appId }}</b>
|
||||
<small>{{ phone.t(`Apps.easyShare.kinds.${payload.kind}`) }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="shared-content-card__media">
|
||||
<video
|
||||
v-if="payload.imageUrl && !imageFailed && isVideo"
|
||||
:src="payload.imageUrl"
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata"
|
||||
@error="imageFailed = true"
|
||||
/>
|
||||
<img
|
||||
v-else-if="payload.imageUrl && !imageFailed"
|
||||
:src="payload.imageUrl"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
@error="imageFailed = true"
|
||||
/>
|
||||
<span v-else><component :is="fallbackIcon" :size="30" /></span>
|
||||
</div>
|
||||
<div class="shared-content-card__copy">
|
||||
<small v-if="payload.subtitle">{{ payload.subtitle }}</small>
|
||||
<strong>{{ payload.title }}</strong>
|
||||
<p v-if="payload.copyText !== payload.title">{{ payload.copyText }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shared-content-card {
|
||||
width: min(262px, 73cqw);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(60 60 67 / 13%);
|
||||
border-radius: 20px;
|
||||
color: #171719;
|
||||
background: rgb(255 255 255 / 96%);
|
||||
box-shadow: 0 8px 24px rgb(19 45 78 / 13%);
|
||||
}
|
||||
|
||||
.shared-content-card__source {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 11px;
|
||||
}
|
||||
|
||||
.shared-content-card__source > img {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
border-radius: 7px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.shared-content-card__source b,
|
||||
.shared-content-card__source small {
|
||||
display: block;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.shared-content-card__source b {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.shared-content-card__source small {
|
||||
margin-top: 2px;
|
||||
color: #8e8e93;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.shared-content-card__media {
|
||||
height: 142px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(145deg, #dcecff, #b9d5ff);
|
||||
}
|
||||
|
||||
.shared-content-card__media > img,
|
||||
.shared-content-card__media > video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.shared-content-card__media > span {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #0a84ff;
|
||||
}
|
||||
|
||||
.shared-content-card__copy {
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.shared-content-card__copy small,
|
||||
.shared-content-card__copy strong,
|
||||
.shared-content-card__copy p {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.shared-content-card__copy small {
|
||||
margin-bottom: 3px;
|
||||
color: #0a84ff;
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.shared-content-card__copy strong {
|
||||
display: -webkit-box;
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.shared-content-card__copy p {
|
||||
display: -webkit-box;
|
||||
margin-top: 5px;
|
||||
color: #636366;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.shared-content-card--compact {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
border-radius: 16px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.shared-content-card--compact .shared-content-card__source {
|
||||
grid-column: 2;
|
||||
padding: 7px 9px 0;
|
||||
}
|
||||
|
||||
.shared-content-card--compact .shared-content-card__media {
|
||||
grid-row: 1 / span 2;
|
||||
height: 78px;
|
||||
}
|
||||
|
||||
.shared-content-card--compact .shared-content-card__copy {
|
||||
grid-column: 2;
|
||||
padding: 5px 9px 8px;
|
||||
}
|
||||
|
||||
.shared-content-card--compact .shared-content-card__copy p,
|
||||
.shared-content-card--compact .shared-content-card__source small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.shared-content-card--darkchat {
|
||||
border-color: rgb(255 255 255 / 10%);
|
||||
color: #f5f5f7;
|
||||
background: linear-gradient(155deg, #302c3d, #1b1922);
|
||||
box-shadow: 0 9px 25px rgb(0 0 0 / 28%);
|
||||
}
|
||||
|
||||
.shared-content-card--darkchat .shared-content-card__media {
|
||||
background: linear-gradient(145deg, #392f5c, #201c31);
|
||||
}
|
||||
|
||||
.shared-content-card--darkchat .shared-content-card__media > span,
|
||||
.shared-content-card--darkchat .shared-content-card__copy small {
|
||||
color: #bf9cff;
|
||||
}
|
||||
|
||||
.shared-content-card--darkchat .shared-content-card__copy p {
|
||||
color: #beb9c8;
|
||||
}
|
||||
|
||||
.shared-content-card--flare {
|
||||
border-color: rgb(255 255 255 / 58%);
|
||||
background: linear-gradient(155deg, #fff, #fff0f6);
|
||||
box-shadow: 0 8px 24px rgb(245 71 132 / 15%);
|
||||
}
|
||||
|
||||
.shared-content-card--flare .shared-content-card__media {
|
||||
background: linear-gradient(145deg, #ffd4e4, #ffc1d5);
|
||||
}
|
||||
|
||||
.shared-content-card--flare .shared-content-card__media > span,
|
||||
.shared-content-card--flare .shared-content-card__copy small {
|
||||
color: #ef3f7c;
|
||||
}
|
||||
|
||||
:global(.phone-app.dark) .shared-content-card--messages {
|
||||
border-color: rgb(255 255 255 / 10%);
|
||||
color: #f5f5f7;
|
||||
background: linear-gradient(155deg, #34363b, #242a33);
|
||||
}
|
||||
|
||||
:global(.phone-app.dark) .shared-content-card--messages .shared-content-card__copy p {
|
||||
color: #b8b8bd;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user