feat(profile): show real telegram avatars in messenger chips
This commit is contained in:
@@ -152,11 +152,14 @@ export enum LoginChannel {
|
||||
|
||||
export type MessengerConnection = {
|
||||
__typename?: 'MessengerConnection';
|
||||
avatarAvailable: Scalars['Boolean']['output'];
|
||||
channelId: Scalars['String']['output'];
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
isActive: Scalars['Boolean']['output'];
|
||||
type: MessengerType;
|
||||
userId: Scalars['ID']['output'];
|
||||
username?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type MessengerDispatchResult = {
|
||||
@@ -619,7 +622,7 @@ export type ConsumeLoginTokenMutation = { __typename?: 'Mutation', consumeLoginT
|
||||
export type MeQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string, email: string } | null };
|
||||
export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string, email: string, fullName: string } | null };
|
||||
|
||||
export type RegisterSelfMutationVariables = Exact<{
|
||||
input: RegisterSelfInput;
|
||||
@@ -650,7 +653,7 @@ export type ClientProductsQuery = { __typename?: 'Query', clientProducts: Array<
|
||||
export type MyMessengerConnectionsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MyMessengerConnectionsQuery = { __typename?: 'Query', myMessengerConnections: Array<{ __typename?: 'MessengerConnection', id: string, type: MessengerType, channelId: string, isActive: boolean }> };
|
||||
export type MyMessengerConnectionsQuery = { __typename?: 'Query', myMessengerConnections: Array<{ __typename?: 'MessengerConnection', id: string, type: MessengerType, channelId: string, displayName?: string | null, username?: string | null, avatarAvailable: boolean, isActive: boolean }> };
|
||||
|
||||
export type MyNotificationHistoryQueryVariables = Exact<{
|
||||
channel: MessengerType;
|
||||
@@ -791,6 +794,7 @@ export const MeDocument = gql`
|
||||
me {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -970,6 +974,9 @@ export const MyMessengerConnectionsDocument = gql`
|
||||
id
|
||||
type
|
||||
channelId
|
||||
displayName
|
||||
username
|
||||
avatarAvailable
|
||||
isActive
|
||||
}
|
||||
}
|
||||
|
||||
51
app/composables/useMessengerConnectionPresentation.ts
Normal file
51
app/composables/useMessengerConnectionPresentation.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
type MessengerConnectionView = {
|
||||
id: string;
|
||||
type: 'TELEGRAM' | 'MAX';
|
||||
channelId: string;
|
||||
displayName?: string | null;
|
||||
username?: string | null;
|
||||
avatarAvailable?: boolean | null;
|
||||
};
|
||||
|
||||
export function messengerConnectionName(connection: MessengerConnectionView | null | undefined) {
|
||||
const displayName = String(connection?.displayName || '').trim();
|
||||
if (displayName) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const username = String(connection?.username || '').trim();
|
||||
if (username) {
|
||||
return `@${username.replace(/^@+/, '')}`;
|
||||
}
|
||||
|
||||
return connection?.channelId || 'Не подключен';
|
||||
}
|
||||
|
||||
export function messengerConnectionHandle(connection: MessengerConnectionView | null | undefined) {
|
||||
const username = String(connection?.username || '').trim().replace(/^@+/, '');
|
||||
if (username) {
|
||||
return `@${username}`;
|
||||
}
|
||||
|
||||
return connection?.channelId || '';
|
||||
}
|
||||
|
||||
export function messengerConnectionInitials(connection: MessengerConnectionView | null | undefined, fallback: string) {
|
||||
const base = messengerConnectionName(connection);
|
||||
const initials = base
|
||||
.split(' ')
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part.charAt(0).toUpperCase())
|
||||
.join('');
|
||||
|
||||
return initials || fallback;
|
||||
}
|
||||
|
||||
export function messengerConnectionAvatarSrc(connection: MessengerConnectionView | null | undefined) {
|
||||
if (!connection?.avatarAvailable || connection.type !== 'TELEGRAM') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `/api/messenger-avatar/${encodeURIComponent(connection.id)}`;
|
||||
}
|
||||
@@ -6,6 +6,12 @@ import {
|
||||
MyNotificationHistoryDocument,
|
||||
SendTestMessengerMessageDocument,
|
||||
} from '~/composables/graphql/generated';
|
||||
import {
|
||||
messengerConnectionAvatarSrc,
|
||||
messengerConnectionHandle,
|
||||
messengerConnectionInitials,
|
||||
messengerConnectionName,
|
||||
} from '~/composables/useMessengerConnectionPresentation';
|
||||
import { useMessengerStart } from '~/composables/useMessengerStart';
|
||||
|
||||
const selectedChannel = ref<'TELEGRAM' | 'MAX'>('TELEGRAM');
|
||||
@@ -117,15 +123,29 @@ async function sendTest() {
|
||||
|
||||
<div class="rounded-2xl border border-[#d6ebde] bg-white/75 p-4">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="min-w-0">
|
||||
<p class="font-semibold">Telegram</p>
|
||||
<p class="text-sm opacity-80">
|
||||
{{
|
||||
telegramConnection
|
||||
? `Подключен: ${telegramConnection.channelId}`
|
||||
: 'Не подключен'
|
||||
}}
|
||||
</p>
|
||||
<div v-if="telegramConnection" class="mt-3 flex items-center gap-3">
|
||||
<div v-if="messengerConnectionAvatarSrc(telegramConnection)" class="avatar">
|
||||
<div class="h-11 w-11 rounded-full">
|
||||
<img :src="messengerConnectionAvatarSrc(telegramConnection)" :alt="messengerConnectionName(telegramConnection)">
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="avatar placeholder">
|
||||
<div class="h-11 w-11 rounded-full bg-[#123824] text-sm font-bold text-white">
|
||||
<span>{{ messengerConnectionInitials(telegramConnection, 'TG') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-[#123824]">
|
||||
{{ messengerConnectionName(telegramConnection) }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-[#5c7b69]">
|
||||
{{ messengerConnectionHandle(telegramConnection) || 'Подключен' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm opacity-80">Не подключен</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -147,15 +167,24 @@ async function sendTest() {
|
||||
|
||||
<div class="rounded-2xl border border-[#d6ebde] bg-white/75 p-4">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="min-w-0">
|
||||
<p class="font-semibold">Max</p>
|
||||
<p class="text-sm opacity-80">
|
||||
{{
|
||||
maxConnection
|
||||
? `Подключен: ${maxConnection.channelId}`
|
||||
: 'Не подключен'
|
||||
}}
|
||||
</p>
|
||||
<div v-if="maxConnection" class="mt-3 flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="h-11 w-11 rounded-full bg-[#2b7fff] text-sm font-bold text-white">
|
||||
<span>{{ messengerConnectionInitials(maxConnection, 'MX') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-[#123824]">
|
||||
{{ messengerConnectionName(maxConnection) }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-[#5c7b69]">
|
||||
{{ messengerConnectionHandle(maxConnection) || 'Подключен' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm opacity-80">Не подключен</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -6,10 +6,21 @@ import {
|
||||
MyMessengerConnectionsDocument,
|
||||
type MyDeliveryAddressesQuery,
|
||||
} from '~/composables/graphql/generated';
|
||||
import {
|
||||
messengerConnectionAvatarSrc,
|
||||
messengerConnectionHandle,
|
||||
messengerConnectionInitials,
|
||||
messengerConnectionName,
|
||||
} from '~/composables/useMessengerConnectionPresentation';
|
||||
import { isCounterpartyProfileComplete } from '~/composables/useCounterpartyProfile';
|
||||
|
||||
type MessengerItem = {
|
||||
id: string;
|
||||
type: 'TELEGRAM' | 'MAX';
|
||||
channelId: string;
|
||||
displayName?: string | null;
|
||||
username?: string | null;
|
||||
avatarAvailable?: boolean | null;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
@@ -22,22 +33,18 @@ const deliveryAddressesQuery = useQuery(MyDeliveryAddressesDocument);
|
||||
const profileIsComplete = computed(() => isCounterpartyProfileComplete(profileQuery.result.value?.myCounterpartyProfile));
|
||||
|
||||
const telegramConnected = computed(() =>
|
||||
Boolean(
|
||||
connectionsQuery.result.value?.myMessengerConnections?.find(
|
||||
(item: MessengerItem) => item.type === 'TELEGRAM' && item.isActive,
|
||||
),
|
||||
connectionsQuery.result.value?.myMessengerConnections?.find(
|
||||
(item: MessengerItem) => item.type === 'TELEGRAM' && item.isActive,
|
||||
),
|
||||
);
|
||||
|
||||
const maxConnected = computed(() =>
|
||||
Boolean(
|
||||
connectionsQuery.result.value?.myMessengerConnections?.find(
|
||||
(item: MessengerItem) => item.type === 'MAX' && item.isActive,
|
||||
),
|
||||
connectionsQuery.result.value?.myMessengerConnections?.find(
|
||||
(item: MessengerItem) => item.type === 'MAX' && item.isActive,
|
||||
),
|
||||
);
|
||||
|
||||
const connectedMessengerCount = computed(() => Number(telegramConnected.value) + Number(maxConnected.value));
|
||||
const connectedMessengerCount = computed(() => Number(Boolean(telegramConnected.value)) + Number(Boolean(maxConnected.value)));
|
||||
|
||||
const companyNamePreview = computed(() => profileQuery.result.value?.myCounterpartyProfile?.companyName?.trim() || '');
|
||||
const notificationsSummary = computed(() => {
|
||||
@@ -81,19 +88,43 @@ const defaultDeliveryAddress = computed(() => deliveryAddresses.value.find((item
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<p class="text-lg font-bold text-[#123824]">Уведомления</p>
|
||||
</div>
|
||||
<div class="mb-2 flex items-center justify-end gap-2">
|
||||
<span
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold"
|
||||
:class="telegramConnected ? 'bg-[#def6ea] text-[#0d854a]' : 'bg-[#eceff3] text-[#6b7280]'"
|
||||
>
|
||||
TG
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold"
|
||||
:class="maxConnected ? 'bg-[#def6ea] text-[#0d854a]' : 'bg-[#eceff3] text-[#6b7280]'"
|
||||
>
|
||||
MX
|
||||
</span>
|
||||
<div class="mb-3 space-y-2">
|
||||
<div v-if="telegramConnected" class="flex items-center gap-3 rounded-2xl bg-[#f8fbf9] px-3 py-2">
|
||||
<div v-if="messengerConnectionAvatarSrc(telegramConnected)" class="avatar">
|
||||
<div class="h-10 w-10 rounded-full">
|
||||
<img :src="messengerConnectionAvatarSrc(telegramConnected)" :alt="messengerConnectionName(telegramConnected)">
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="avatar placeholder">
|
||||
<div class="h-10 w-10 rounded-full bg-[#123824] text-xs font-bold text-white">
|
||||
<span>{{ messengerConnectionInitials(telegramConnected, 'TG') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-[#123824]">
|
||||
{{ messengerConnectionName(telegramConnected) }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-[#5c7b69]">
|
||||
{{ messengerConnectionHandle(telegramConnected) || 'Telegram подключен' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="maxConnected" class="flex items-center gap-3 rounded-2xl bg-[#f8fbf9] px-3 py-2">
|
||||
<div class="avatar placeholder">
|
||||
<div class="h-10 w-10 rounded-full bg-[#2b7fff] text-xs font-bold text-white">
|
||||
<span>{{ messengerConnectionInitials(maxConnected, 'MX') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-[#123824]">
|
||||
{{ messengerConnectionName(maxConnected) }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-[#5c7b69]">
|
||||
{{ messengerConnectionHandle(maxConnected) || 'Max подключен' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-[#355947]">{{ notificationsSummary }}</p>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -4,12 +4,22 @@ import {
|
||||
MeDocument,
|
||||
MyMessengerConnectionsDocument,
|
||||
} from '~/composables/graphql/generated';
|
||||
import {
|
||||
messengerConnectionAvatarSrc,
|
||||
messengerConnectionHandle,
|
||||
messengerConnectionInitials,
|
||||
messengerConnectionName,
|
||||
} from '~/composables/useMessengerConnectionPresentation';
|
||||
import { useMessengerStart } from '~/composables/useMessengerStart';
|
||||
|
||||
type MessengerItem = {
|
||||
id: string;
|
||||
type: 'TELEGRAM' | 'MAX';
|
||||
isActive: boolean;
|
||||
channelId: string;
|
||||
displayName?: string | null;
|
||||
username?: string | null;
|
||||
avatarAvailable?: boolean | null;
|
||||
};
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
@@ -63,6 +73,18 @@ const successText = computed(() =>
|
||||
? 'Теперь этот Telegram привязан к вашему кабинету, и уведомления будут приходить в него.'
|
||||
: 'Теперь этот Max привязан к вашему кабинету, и уведомления будут приходить в него.',
|
||||
);
|
||||
const successConnection = computed(() =>
|
||||
successChannel.value === 'telegram' ? telegramConnection.value : maxConnection.value,
|
||||
);
|
||||
const successAvatarSrc = computed(() => messengerConnectionAvatarSrc(successConnection.value));
|
||||
const successAvatarInitials = computed(() =>
|
||||
messengerConnectionInitials(
|
||||
successConnection.value,
|
||||
profileInitials.value,
|
||||
),
|
||||
);
|
||||
const successConnectionName = computed(() => messengerConnectionName(successConnection.value));
|
||||
const successConnectionHandleValue = computed(() => messengerConnectionHandle(successConnection.value));
|
||||
|
||||
onMounted(() => {
|
||||
if (showSuccess.value) {
|
||||
@@ -92,18 +114,24 @@ async function connectMessenger(channel: 'TELEGRAM' | 'MAX') {
|
||||
|
||||
<div v-if="showSuccess" class="surface-card rounded-3xl border border-[#c7efd7] bg-[#f4fff8] p-5">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center">
|
||||
<div class="avatar placeholder">
|
||||
<div v-if="successAvatarSrc" class="avatar">
|
||||
<div class="h-18 w-18 rounded-full ring ring-[#c7efd7] ring-offset-2 ring-offset-white">
|
||||
<img :src="successAvatarSrc" :alt="successConnectionName">
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="avatar placeholder">
|
||||
<div class="h-18 w-18 rounded-full bg-[#123824] text-xl font-bold text-white">
|
||||
<span>{{ profileInitials }}</span>
|
||||
<span>{{ successAvatarInitials }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="badge badge-success badge-outline">Успешно</div>
|
||||
<h2 class="text-2xl font-extrabold text-[#0f2f20]">{{ successTitle }}</h2>
|
||||
<p class="text-sm text-[#355947]">
|
||||
{{ profileName }}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm text-[#355947]">
|
||||
<span class="font-semibold text-[#123824]">{{ successConnectionName }}</span>
|
||||
<span v-if="successConnectionHandleValue">{{ successConnectionHandleValue }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-[#355947]">
|
||||
{{ successText }}
|
||||
</p>
|
||||
@@ -119,9 +147,27 @@ async function connectMessenger(channel: 'TELEGRAM' | 'MAX') {
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="rounded-2xl bg-[#f8fbf9] p-4 transition hover:shadow-md">
|
||||
<p class="font-semibold">Telegram</p>
|
||||
<p class="text-sm opacity-80">
|
||||
{{ telegramConnection ? `Подключен: ${telegramConnection.channelId}` : 'Не подключен' }}
|
||||
</p>
|
||||
<div v-if="telegramConnection" class="mt-3 flex items-center gap-3 rounded-2xl bg-white px-3 py-2">
|
||||
<div v-if="messengerConnectionAvatarSrc(telegramConnection)" class="avatar">
|
||||
<div class="h-11 w-11 rounded-full">
|
||||
<img :src="messengerConnectionAvatarSrc(telegramConnection)" :alt="messengerConnectionName(telegramConnection)">
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="avatar placeholder">
|
||||
<div class="h-11 w-11 rounded-full bg-[#123824] text-sm font-bold text-white">
|
||||
<span>{{ messengerConnectionInitials(telegramConnection, 'TG') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-[#123824]">
|
||||
{{ messengerConnectionName(telegramConnection) }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-[#5c7b69]">
|
||||
{{ messengerConnectionHandle(telegramConnection) || 'Подключен' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm opacity-80">Не подключен</p>
|
||||
<button
|
||||
class="btn btn-secondary mt-3 w-full"
|
||||
:class="{ 'btn-disabled pointer-events-none': !telegramConnectUrl }"
|
||||
@@ -140,9 +186,22 @@ async function connectMessenger(channel: 'TELEGRAM' | 'MAX') {
|
||||
|
||||
<div class="rounded-2xl bg-[#f8fbf9] p-4 transition hover:shadow-md">
|
||||
<p class="font-semibold">Max</p>
|
||||
<p class="text-sm opacity-80">
|
||||
{{ maxConnection ? `Подключен: ${maxConnection.channelId}` : 'Не подключен' }}
|
||||
</p>
|
||||
<div v-if="maxConnection" class="mt-3 flex items-center gap-3 rounded-2xl bg-white px-3 py-2">
|
||||
<div class="avatar placeholder">
|
||||
<div class="h-11 w-11 rounded-full bg-[#2b7fff] text-sm font-bold text-white">
|
||||
<span>{{ messengerConnectionInitials(maxConnection, 'MX') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-[#123824]">
|
||||
{{ messengerConnectionName(maxConnection) }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-[#5c7b69]">
|
||||
{{ messengerConnectionHandle(maxConnection) || 'Подключен' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm opacity-80">Не подключен</p>
|
||||
<button
|
||||
class="btn btn-accent mt-3 w-full"
|
||||
:class="{ 'btn-disabled pointer-events-none': !maxConnectUrl }"
|
||||
|
||||
@@ -2,5 +2,6 @@ query Me {
|
||||
me {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ query MyMessengerConnections {
|
||||
id
|
||||
type
|
||||
channelId
|
||||
displayName
|
||||
username
|
||||
avatarAvailable
|
||||
isActive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,9 @@ type MessengerConnection {
|
||||
userId: ID!
|
||||
type: MessengerType!
|
||||
channelId: String!
|
||||
displayName: String
|
||||
username: String
|
||||
avatarAvailable: Boolean!
|
||||
isActive: Boolean!
|
||||
}
|
||||
|
||||
|
||||
32
server/api/messenger-avatar/[connectionId].get.ts
Normal file
32
server/api/messenger-avatar/[connectionId].get.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig(event);
|
||||
const backendUrl = new URL(config.backendGraphqlUrl);
|
||||
const connectionId = getRouterParam(event, 'connectionId');
|
||||
|
||||
if (!connectionId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Connection id is required.',
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(`${backendUrl.origin}/messenger/avatar/${encodeURIComponent(connectionId)}`, {
|
||||
headers: {
|
||||
...(getHeader(event, 'cookie') ? { cookie: getHeader(event, 'cookie') as string } : {}),
|
||||
...(getHeader(event, 'authorization') ? { authorization: getHeader(event, 'authorization') as string } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: 'Messenger avatar is not available.',
|
||||
});
|
||||
}
|
||||
|
||||
setResponseStatus(event, response.status);
|
||||
setHeader(event, 'content-type', response.headers.get('content-type') || 'image/jpeg');
|
||||
setHeader(event, 'cache-control', response.headers.get('cache-control') || 'private, max-age=300');
|
||||
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
});
|
||||
Reference in New Issue
Block a user