update ui/ux

This commit is contained in:
2025-11-30 19:38:17 -06:00
parent 52558a7167
commit 5967cd827f
6 changed files with 280 additions and 168 deletions

View File

@@ -1,5 +1,5 @@
<template>
<button type="button" class="text-gray-700 bg-gray-100 dark:bg-zinc-600 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500 hover:bg-gray-200 p-2 rounded-full">
<button type="button" class="text-gray-700 bg-gray-100 dark:bg-zinc-600 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500 hover:bg-gray-200 p-2 rounded-full w-8 h-8 flex items-center justify-center flex-shrink-0">
<slot/>
</button>
</template>

View File

@@ -1,35 +1,35 @@
<template>
<!-- peer selected -->
<div v-if="selectedPeer" class="flex flex-col h-full bg-white/90 dark:bg-zinc-950/80 backdrop-blur overflow-hidden sm:m-3 sm:border sm:rounded-2xl sm:shadow-xl border-gray-200 dark:border-zinc-800 transition-colors">
<div v-if="selectedPeer" class="flex flex-col h-full bg-white dark:bg-zinc-950 overflow-hidden sm:m-3 sm:border sm:rounded-2xl sm:shadow-lg border-gray-200/50 dark:border-zinc-800/50 transition-all">
<!-- header -->
<div class="flex p-2 border-b border-gray-300 dark:border-zinc-800">
<div class="flex items-center px-4 py-3 border-b border-gray-200/60 dark:border-zinc-800/60 bg-white/80 dark:bg-zinc-900/50 backdrop-blur-sm">
<!-- peer icon -->
<div class="my-auto mr-2">
<div v-if="selectedPeer.lxmf_user_icon" class="p-2 rounded" :style="{ 'color': selectedPeer.lxmf_user_icon.foreground_colour, 'background-color': selectedPeer.lxmf_user_icon.background_colour }">
<MaterialDesignIcon :icon-name="selectedPeer.lxmf_user_icon.icon_name" class="w-6 h-6"/>
<div class="flex-shrink-0 mr-3">
<div v-if="selectedPeer.lxmf_user_icon" class="p-2.5 rounded-xl shadow-sm" :style="{ 'color': selectedPeer.lxmf_user_icon.foreground_colour, 'background-color': selectedPeer.lxmf_user_icon.background_colour }">
<MaterialDesignIcon :icon-name="selectedPeer.lxmf_user_icon.icon_name" class="w-5 h-5"/>
</div>
<div v-else class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded">
<MaterialDesignIcon icon-name="account-outline" class="w-6 h-6"/>
<div v-else class="bg-gradient-to-br from-gray-100 to-gray-200 dark:from-zinc-800 dark:to-zinc-700 text-gray-600 dark:text-gray-300 p-2.5 rounded-xl shadow-sm">
<MaterialDesignIcon icon-name="account-outline" class="w-5 h-5"/>
</div>
</div>
<!-- peer info -->
<div class="min-w-0 flex-1">
<div @click="updateCustomDisplayName" class="flex cursor-pointer min-w-0">
<div v-if="selectedPeer.custom_display_name != null" class="my-auto mr-1 dark:text-white" title="Custom Display Name">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<div @click="updateCustomDisplayName" class="flex items-center cursor-pointer min-w-0 group">
<div v-if="selectedPeer.custom_display_name != null" class="mr-1.5 text-gray-500 dark:text-zinc-400 group-hover:text-gray-700 dark:group-hover:text-zinc-200 transition-colors" title="Custom Display Name">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
</svg>
</div>
<div class="my-auto font-semibold dark:text-white truncate max-w-xs sm:max-w-sm" :title="selectedPeer.custom_display_name ?? selectedPeer.display_name">
<div class="font-semibold text-gray-900 dark:text-zinc-100 truncate max-w-xs sm:max-w-sm text-base" :title="selectedPeer.custom_display_name ?? selectedPeer.display_name">
{{ selectedPeer.custom_display_name ?? selectedPeer.display_name }}
</div>
</div>
<div class="text-sm dark:text-zinc-300">
<div class="text-xs text-gray-500 dark:text-zinc-400 mt-0.5">
<!-- destination hash -->
<div class="inline-block mr-1">
@@ -64,25 +64,21 @@
</div>
<!-- dropdown menu -->
<div class="ml-auto my-auto mx-2">
<div class="ml-auto flex items-center gap-1">
<ConversationDropDownMenu
v-if="selectedPeer"
:peer="selectedPeer"
@conversation-deleted="onConversationDeleted"
@set-custom-display-name="updateCustomDisplayName"/>
</div>
<!-- popout button -->
<div class="my-auto mr-2">
<IconButton @click="openConversationPopout" title="Pop out chat">
<MaterialDesignIcon icon-name="open-in-new" class="w-5 h-5"/>
<!-- popout button -->
<IconButton @click="openConversationPopout" title="Pop out chat" class="text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-200">
<MaterialDesignIcon icon-name="open-in-new" class="w-4 h-4"/>
</IconButton>
</div>
<!-- close button -->
<div class="my-auto mr-2">
<IconButton @click="close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<!-- close button -->
<IconButton @click="close" class="text-gray-500 dark:text-zinc-400 hover:text-gray-700 dark:hover:text-zinc-200">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
</svg>
</IconButton>
@@ -91,27 +87,33 @@
</div>
<!-- chat items -->
<div @scroll="onMessagesScroll" id="messages" class="h-full overflow-y-scroll">
<div @scroll="onMessagesScroll" id="messages" class="h-full overflow-y-scroll bg-gray-50/30 dark:bg-zinc-950/50">
<div v-if="selectedPeerChatItems.length > 0" class="flex flex-col flex-col-reverse p-3">
<div v-if="selectedPeerChatItems.length > 0" class="flex flex-col flex-col-reverse px-4 py-6">
<div v-for="chatItem of selectedPeerChatItemsReversed" :key="chatItem.lxmf_message.hash" class="flex flex-col max-w-xl mt-3" :class="{ 'ml-auto pl-4 md:pl-16 items-end': chatItem.is_outbound, 'mr-auto pr-4 md:pr-16 items-start': !chatItem.is_outbound }">
<div v-for="chatItem of selectedPeerChatItemsReversed" :key="chatItem.lxmf_message.hash" class="flex flex-col max-w-[75%] sm:max-w-[65%] lg:max-w-[55%] mb-4 group" :class="{ 'ml-auto items-end': chatItem.is_outbound, 'mr-auto items-start': !chatItem.is_outbound }">
<!-- message content -->
<div @click="onChatItemClick(chatItem)" class="border border-gray-300 dark:border-zinc-800 rounded-xl shadow overflow-hidden" :class="[ ['cancelled', 'failed'].includes(chatItem.lxmf_message.state) ? 'bg-red-500 text-white' : chatItem.is_outbound ? 'bg-[#3b82f6] text-white' : 'bg-[#efefef]' ]">
<div @click="onChatItemClick(chatItem)" class="relative rounded-2xl overflow-hidden transition-all duration-200 hover:shadow-md" :class="[
['cancelled', 'failed'].includes(chatItem.lxmf_message.state)
? 'bg-red-500 text-white shadow-sm'
: chatItem.is_outbound
? 'bg-blue-600 text-white shadow-sm'
: 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-sm'
]">
<div class="w-full space-y-0.5 px-2.5 py-1">
<div class="w-full space-y-1 px-4 py-2.5">
<!-- content -->
<div v-if="chatItem.lxmf_message.content" style="white-space:pre-wrap;word-break:break-word;font-family:inherit;">{{ chatItem.lxmf_message.content }}</div>
<div v-if="chatItem.lxmf_message.content" class="text-sm leading-relaxed whitespace-pre-wrap break-words" style="font-family:inherit;">{{ chatItem.lxmf_message.content }}</div>
<!-- image field -->
<div v-if="chatItem.lxmf_message.fields?.image" class="relative group">
<div v-if="chatItem.lxmf_message.fields?.image" class="relative group mt-1 -mx-1">
<img
@click.stop="openImage(`data:image/${chatItem.lxmf_message.fields.image.image_type};base64,${chatItem.lxmf_message.fields.image.image_bytes}`)"
:src="`data:image/${chatItem.lxmf_message.fields.image.image_type};base64,${chatItem.lxmf_message.fields.image.image_bytes}`"
class="w-full rounded-md cursor-pointer"/>
<div class="absolute bottom-1 left-1 bg-black/70 text-white text-xs px-2 py-0.5 rounded-full flex space-x-1">
class="w-full rounded-lg cursor-pointer transition-transform group-hover:scale-[1.01]"/>
<div class="absolute bottom-2 left-2 bg-black/60 backdrop-blur-sm text-white text-xs px-2.5 py-1 rounded-lg flex items-center gap-1.5">
<span>{{ (chatItem.lxmf_message.fields.image.image_type ?? 'image').toUpperCase() }}</span>
<span></span>
<span>{{ formatBase64Bytes(chatItem.lxmf_message.fields.image.image_bytes) }}</span>
@@ -122,14 +124,12 @@
<div v-if="chatItem.lxmf_message.fields?.audio" class="pb-1">
<!-- audio is loaded -->
<audio v-if="lxmfMessageAudioAttachmentCache[chatItem.lxmf_message.hash]" controls class="shadow rounded-full" style="height:54px;">
<source :src="lxmfMessageAudioAttachmentCache[chatItem.lxmf_message.hash]" type="audio/wav"/>
</audio>
<audio v-if="lxmfMessageAudioAttachmentCache[chatItem.lxmf_message.hash]" controls class="w-full rounded-lg shadow-sm" style="height:54px;" :class="chatItem.is_outbound ? 'audio-controls-light' : 'audio-controls-dark'"></audio>
<!-- audio is not yet loaded -->
<!-- min height to make sure audio player doesn't cause height increase after loading -->
<div v-else style="min-height:54px;" class="flex">
<button @click="downloadFileFromBase64('audio.bin', chatItem.lxmf_message.fields.audio.audio_bytes)" type="button" class="my-auto flex border border-gray-300 dark:border-zinc-800 hover:bg-gray-100 rounded px-2 py-1 text-sm text-gray-700 font-semibold cursor-pointer space-x-2 bg-[#efefef]">
<button @click="downloadFileFromBase64('audio.bin', chatItem.lxmf_message.fields.audio.audio_bytes)" type="button" class="my-auto flex items-center gap-2 border border-gray-200/60 dark:border-zinc-700 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-lg px-3 py-2 text-sm font-medium transition-colors" :class="chatItem.is_outbound ? 'bg-white/20 text-white border-white/20 hover:bg-white/30' : 'bg-gray-50 dark:bg-zinc-800/50 text-gray-700 dark:text-zinc-300'">
<span class="my-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m9 9 10.5-3m0 6.553v3.75a2.25 2.25 0 0 1-1.632 2.163l-1.32.377a1.803 1.803 0 1 1-.99-3.467l2.31-.66a2.25 2.25 0 0 0 1.632-2.163Zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 0 1-1.632 2.163l-1.32.377a1.803 1.803 0 0 1-.99-3.467l2.31-.66A2.25 2.25 0 0 0 9 15.553Z" />
@@ -146,13 +146,13 @@
</button>
</div>
<div class="text-xs mt-1" :class="chatItem.is_outbound ? 'text-white/80' : 'text-gray-600'">
<div class="text-xs mt-1.5" :class="chatItem.is_outbound ? 'text-white/70' : 'text-gray-500 dark:text-zinc-400'">
Audio • {{ formatBase64Bytes(chatItem.lxmf_message.fields.audio.audio_bytes) }}
</div>
</div>
<!-- file attachment fields -->
<div v-if="chatItem.lxmf_message.fields?.file_attachments" class="space-y-1">
<div v-if="chatItem.lxmf_message.fields?.file_attachments" class="space-y-2 mt-1">
<a
v-for="file_attachment of chatItem.lxmf_message.fields?.file_attachments ?? []"
:key="file_attachment.file_name"
@@ -160,15 +160,15 @@
target="_blank"
:download="file_attachment.file_name"
:href="`data:application/octet-stream;base64,${file_attachment.file_bytes}`"
class="flex border border-gray-300 dark:border-zinc-800 hover:bg-gray-100 rounded px-2 py-1 text-sm text-gray-700 font-semibold cursor-pointer space-x-2 bg-[#efefef]">
class="flex items-center gap-3 border rounded-lg px-3 py-2 text-sm font-medium cursor-pointer transition-colors" :class="chatItem.is_outbound ? 'bg-white/20 text-white border-white/20 hover:bg-white/30' : 'bg-gray-50 dark:bg-zinc-800/50 text-gray-700 dark:text-zinc-300 border-gray-200/60 dark:border-zinc-700 hover:bg-gray-100 dark:hover:bg-zinc-800'">
<div class="my-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13"></path>
</svg>
</div>
<div class="my-auto w-full">
<div>{{ file_attachment.file_name }}</div>
<div class="text-xs font-normal text-gray-500">{{ formatBase64Bytes(file_attachment.file_bytes) }}</div>
<div class="flex-1 min-w-0">
<div class="truncate">{{ file_attachment.file_name }}</div>
<div class="text-xs font-normal mt-0.5" :class="chatItem.is_outbound ? 'text-white/60' : 'text-gray-500 dark:text-zinc-400'">{{ formatBase64Bytes(file_attachment.file_bytes) }}</div>
</div>
<div class="my-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
@@ -181,10 +181,10 @@
</div>
<!-- actions -->
<div v-if="chatItem.is_actions_expanded" class="border-t p-1 bg-[#efefef] text-white">
<div v-if="chatItem.is_actions_expanded" class="border-t px-4 py-2.5" :class="chatItem.is_outbound ? 'border-white/20 bg-white/10' : 'border-gray-200/60 dark:border-zinc-800/60 bg-gray-50/50 dark:bg-zinc-900/50'">
<!-- delete message -->
<button @click.stop="deleteChatItem(chatItem)" type="button" class="inline-flex items-center gap-x-1 rounded-md bg-red-500 px-2 py-1 text-xs font-semibold text-white shadow-sm hover:bg-red-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500">
<button @click.stop="deleteChatItem(chatItem)" type="button" class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-red-600 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500">
Delete
</button>
@@ -193,12 +193,12 @@
</div>
<!-- message state -->
<div v-if="chatItem.is_outbound" class="flex text-right" :class="[ ['cancelled', 'failed'].includes(chatItem.lxmf_message.state) ? 'text-red-500' : 'text-gray-500' ]">
<div class="flex ml-auto space-x-1">
<div v-if="chatItem.is_outbound" class="flex text-right mt-1.5 px-1" :class="[ ['cancelled', 'failed'].includes(chatItem.lxmf_message.state) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-zinc-500' ]">
<div class="flex ml-auto items-center space-x-1.5 text-xs">
<!-- state label -->
<div class="my-auto">
<span @click="showSentMessageInfo(chatItem.lxmf_message)" class="space-x-1 cursor-pointer">
<span @click="toggleSentMessageInfo(chatItem.lxmf_message.hash)" class="space-x-1 cursor-pointer hover:underline">
<span>{{ chatItem.lxmf_message.state }}</span>
<span v-if="chatItem.lxmf_message.state === 'outbound' && chatItem.lxmf_message.delivery_attempts >= 1">(attempt {{ chatItem.lxmf_message.delivery_attempts + 1 }})</span>
<span v-if="chatItem.lxmf_message.state === 'sent' && chatItem.lxmf_message.method === 'opportunistic' && chatItem.lxmf_message.delivery_attempts >= 1">(attempt {{ chatItem.lxmf_message.delivery_attempts }})</span>
@@ -241,18 +241,23 @@
</div>
<!-- inbound message info -->
<div v-if="!chatItem.is_outbound" class="text-xs text-gray-500 mt-0.5 flex flex-col">
<div v-if="!chatItem.is_outbound" class="text-xs text-gray-400 dark:text-zinc-500 mt-1.5 px-1 flex flex-col">
<!-- received timestamp -->
<span @click="showReceivedMessageInfo(chatItem.lxmf_message)" class="cursor-pointer">{{ formatTimeAgo(chatItem.lxmf_message.created_at) }}</span>
<span @click="toggleReceivedMessageInfo(chatItem.lxmf_message.hash)" class="cursor-pointer hover:underline">{{ formatTimeAgo(chatItem.lxmf_message.created_at) }}</span>
</div>
<!-- expanded message details -->
<div v-if="expandedMessageInfo === chatItem.lxmf_message.hash" class="mt-2 px-1 text-xs text-gray-500 dark:text-zinc-400 space-y-0.5">
<div v-for="(line, index) in getMessageInfoLines(chatItem.lxmf_message, chatItem.is_outbound)" :key="index">{{ line }}</div>
</div>
</div>
<!-- load previous -->
<button v-show="!isLoadingPrevious && hasMorePrevious" id="load-previous" @click="loadPrevious" type="button" class="flex space-x-2 mx-auto bg-gray-200 px-3 py-1 hover:bg-gray-300 rounded-full shadow">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<button v-show="!isLoadingPrevious && hasMorePrevious" id="load-previous" @click="loadPrevious" type="button" class="flex items-center gap-2 mx-auto mt-4 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span>Load Previous</span>
@@ -263,8 +268,8 @@
</div>
<!-- send message -->
<div class="w-full border-gray-300 dark:border-zinc-800 border-t p-2">
<div class="mx-auto">
<div class="w-full border-t border-gray-200/60 dark:border-zinc-800/60 bg-white/80 dark:bg-zinc-900/50 backdrop-blur-sm px-3 sm:px-4 py-2.5">
<div class="w-full">
<!-- message composer -->
<div>
@@ -289,9 +294,9 @@
<div class="attachment-card__body w-full">
<div class="attachment-card__title">Voice Note</div>
<div class="attachment-card__meta">{{ formatBytes(newMessageAudio.audio_blob.size) }}</div>
<audio controls class="w-full mt-2 rounded-full bg-white/60 dark:bg-zinc-800/70">
<source :src="newMessageAudio.audio_preview_url" type="audio/wav"/>
</audio>
<audio controls class="w-full mt-2 rounded-lg shadow-sm audio-controls-dark" style="height:54px;">
<source :src="newMessageAudio.audio_preview_url" type="audio/wav"/>
</audio>
</div>
<button @click="removeAudioAttachment" type="button" class="attachment-card__remove">
<MaterialDesignIcon icon-name="delete" class="w-4 h-4"/>
@@ -321,12 +326,12 @@
v-model="newMessageText"
@keydown.enter.exact.native.prevent="onEnterPressed"
@keydown.enter.shift.exact.native.prevent="onShiftEnterPressed"
class="bg-gray-50 border border-gray-300 dark:border-zinc-800 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-zinc-800 dark:text-zinc-100 dark:border-zinc-900"
rows="3"
placeholder="Send a message..."></textarea>
class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 block w-full px-3 sm:px-4 py-2 resize-none shadow-sm transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500"
rows="2"
placeholder="Type a message..."></textarea>
<!-- action button -->
<div class="flex flex-wrap gap-2 items-center mt-3">
<div class="flex flex-wrap gap-2 items-center mt-2">
<button @click="addFilesToMessage" type="button" class="attachment-action-button">
<MaterialDesignIcon icon-name="paperclip-plus" class="w-4 h-4"/>
<span>Add Files</span>
@@ -359,20 +364,91 @@
</div>
<!-- no peer selected -->
<div v-else class="flex flex-col mx-auto my-auto text-center leading-5">
<div class="mx-auto mb-1 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
</svg>
<div v-else class="flex flex-col h-full">
<div class="flex-1 flex flex-col items-center justify-center text-center px-4">
<div class="mb-4">
<div class="w-16 h-16 mx-auto rounded-2xl bg-gradient-to-br from-blue-100 to-blue-200 dark:from-blue-900/30 dark:to-blue-800/30 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 text-blue-600 dark:text-blue-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
</svg>
</div>
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-zinc-100 mb-1">No Active Chat</h3>
<p class="text-sm text-gray-500 dark:text-zinc-400 mb-6">Select a peer from the sidebar or compose a new message below</p>
</div>
<div class="font-semibold dark:text-white">No Active Chat</div>
<div class='dark:text-zinc-300'>Select a Peer to start chatting!</div>
<div class="mx-auto mt-2">
<button @click.stop="openLXMFAddress" type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500
dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500">
Enter an LXMF Address
</button>
<!-- compose message input -->
<div class="w-full border-t border-gray-200/60 dark:border-zinc-800/60 bg-white/80 dark:bg-zinc-900/50 backdrop-blur-sm px-3 sm:px-4 py-2.5">
<div class="w-full">
<div class="space-y-2">
<div v-if="newMessageImage" class="attachment-card">
<div class="attachment-card__preview" @click.stop="openImage(newMessageImageUrl)">
<img v-if="newMessageImageUrl" :src="newMessageImageUrl" class="w-full h-full object-cover rounded-lg"/>
</div>
<div class="attachment-card__body">
<div class="attachment-card__title">Image Attachment</div>
<div class="attachment-card__meta">{{ formatBytes(newMessageImage.size) }}</div>
</div>
<button @click.stop="removeImageAttachment" type="button" class="attachment-card__remove">
<MaterialDesignIcon icon-name="close" class="w-4 h-4"/>
</button>
</div>
<div v-if="newMessageAudio" class="attachment-card">
<div class="attachment-card__body w-full">
<div class="attachment-card__title">Voice Note</div>
<div class="attachment-card__meta">{{ formatBytes(newMessageAudio.audio_blob.size) }}</div>
<audio controls class="w-full mt-2 rounded-lg shadow-sm audio-controls-dark" style="height:54px;">
<source :src="newMessageAudio.audio_preview_url" type="audio/wav"/>
</audio>
</div>
<button @click="removeAudioAttachment" type="button" class="attachment-card__remove">
<MaterialDesignIcon icon-name="delete" class="w-4 h-4"/>
</button>
</div>
<div v-if="newMessageFiles.length > 0" class="flex flex-wrap gap-2">
<div v-for="file in newMessageFiles" :key="file.name + file.size" class="attachment-chip">
<div class="flex items-center gap-2">
<MaterialDesignIcon icon-name="paperclip" class="w-4 h-4 text-gray-500 dark:text-gray-300"/>
<div class="text-sm text-gray-800 dark:text-gray-200 truncate max-w-[160px]">{{ file.name }}</div>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ formatBytes(file.size) }}</span>
</div>
<button @click="removeFileAttachment(file)" type="button" class="attachment-chip__remove">
<MaterialDesignIcon icon-name="close" class="w-3.5 h-3.5"/>
</button>
</div>
</div>
</div>
<div class="flex items-end gap-2 mt-2">
<textarea
ref="compose-input"
id="compose-input"
:readonly="isSendingMessage"
v-model="composeAddress"
@keydown.enter.exact.prevent="onComposeEnterPressed"
class="flex-1 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-3 sm:px-4 py-2 resize-none shadow-sm transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500"
rows="1"
placeholder="Enter LXMF address to compose..."></textarea>
<button @click="onComposeSubmit" type="button" :disabled="!composeAddress || composeAddress.trim() === ''"
class="inline-flex items-center justify-center rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 disabled:bg-gray-400 dark:disabled:bg-zinc-500 disabled:cursor-not-allowed px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button>
</div>
<div class="flex flex-wrap gap-2 items-center mt-2">
<button @click="addFilesToMessage" type="button" class="attachment-action-button">
<MaterialDesignIcon icon-name="paperclip-plus" class="w-4 h-4"/>
<span>Add Files</span>
</button>
<AddImageButton @add-image="onImageSelected"/>
<AddAudioButton
:is-recording-audio-attachment="isRecordingAudioAttachment"
@start-recording="startRecordingAudioAttachment($event)"
@stop-recording="stopRecordingAudioAttachment">
<span>Recording: {{ audioAttachmentRecordingDuration }}</span>
</AddAudioButton>
</div>
</div>
</div>
</div>
@@ -429,6 +505,7 @@ export default {
newMessageFiles: [],
isSendingMessage: false,
autoScrollOnNewMessage: true,
composeAddress: "",
isRecordingAudioAttachment: false,
audioAttachmentMicrophoneRecorder: null,
@@ -437,6 +514,7 @@ export default {
audioAttachmentRecordingDuration: null,
audioAttachmentRecordingTimer: null,
lxmfMessageAudioAttachmentCache: {},
expandedMessageInfo: null,
lxmfAudioModeToCodec2ModeMap: {
// https://github.com/markqvist/LXMF/blob/master/LXMF/LXMF.py#L21
0x01: "450PWB", // AM_CODEC2_450PWB
@@ -455,12 +533,16 @@ export default {
beforeUnmount() {
// stop listening for websocket messages
WebSocketConnection.off("message", this.onWebsocketMessage);
GlobalEmitter.off("compose-new-message", this.onComposeNewMessageEvent);
},
mounted() {
// listen for websocket messages
WebSocketConnection.on("message", this.onWebsocketMessage);
// listen for compose new message event
GlobalEmitter.on("compose-new-message", this.onComposeNewMessageEvent);
},
methods: {
close() {
@@ -597,6 +679,37 @@ export default {
openLXMFAddress() {
GlobalEmitter.emit("compose-new-message");
},
onComposeNewMessageEvent(destinationHash) {
if(!this.selectedPeer && !destinationHash){
this.$nextTick(() => {
const composeInput = document.getElementById("compose-input");
if(composeInput){
composeInput.focus();
}
});
}
},
async onComposeSubmit() {
if(!this.composeAddress || this.composeAddress.trim() === ""){
return;
}
let destinationHash = this.composeAddress.trim();
this.composeAddress = "";
await this.handleComposeAddress(destinationHash);
},
onComposeEnterPressed() {
this.onComposeSubmit();
},
async handleComposeAddress(destinationHash) {
if(destinationHash.startsWith("lxmf@")){
destinationHash = destinationHash.replace("lxmf@", "");
}
if(destinationHash.length !== 32){
DialogUtils.alert("Invalid Address");
return;
}
GlobalEmitter.emit("compose-new-message", destinationHash);
},
onLxmfMessageReceived(lxmfMessage) {
// add inbound message to ui
@@ -1464,89 +1577,64 @@ export default {
this.$emit("reload-conversations");
},
showSentMessageInfo: function(lxmfMessage) {
// basic info
const info = [
`Created: ${Utils.convertUnixMillisToLocalDateTimeString(lxmfMessage.timestamp * 1000)}`,
`Method: ${lxmfMessage.method ?? "unknown"}`,
];
// add audio attachment size
if(lxmfMessage.fields?.audio?.audio_bytes){
const audioBytesLength = atob(lxmfMessage.fields?.audio?.audio_bytes).length;
info.push(`Audio Attachment: ${this.formatBytes(audioBytesLength)}`);
toggleSentMessageInfo: function(messageHash) {
if(this.expandedMessageInfo === messageHash){
this.expandedMessageInfo = null;
} else {
this.expandedMessageInfo = messageHash;
}
// add image attachment size
if(lxmfMessage.fields?.image?.image_bytes){
const imageBytesLength = atob(lxmfMessage.fields?.image?.image_bytes).length;
info.push(`Image Attachment: ${this.formatBytes(imageBytesLength)}`);
}
// add file attachments size
if(lxmfMessage.fields?.file_attachments){
var filesLength = 0;
for(const fileAttachment of lxmfMessage.fields?.file_attachments){
const fileBytesLength = atob(fileAttachment.file_bytes).length;
filesLength += fileBytesLength;
}
info.push(`File Attachments: ${this.formatBytes(filesLength)}`);
}
// show message info
DialogUtils.alert(info.join("\n"));
},
showReceivedMessageInfo: function(lxmfMessage) {
toggleReceivedMessageInfo: function(messageHash) {
if(this.expandedMessageInfo === messageHash){
this.expandedMessageInfo = null;
} else {
this.expandedMessageInfo = messageHash;
}
},
getMessageInfoLines: function(lxmfMessage, isOutbound) {
const lines = [];
// basic info
const info = [
`Sent: ${Utils.convertUnixMillisToLocalDateTimeString(lxmfMessage.timestamp * 1000)}`,
`Received: ${Utils.convertDateTimeToLocalDateTimeString(new Date(lxmfMessage.created_at))}`,
`Method: ${lxmfMessage.method ?? "unknown"}`,
];
if(isOutbound){
lines.push(`Created: ${Utils.convertUnixMillisToLocalDateTimeString(lxmfMessage.timestamp * 1000)}`);
} else {
lines.push(`Sent: ${Utils.convertUnixMillisToLocalDateTimeString(lxmfMessage.timestamp * 1000)}`);
lines.push(`Received: ${Utils.convertDateTimeToLocalDateTimeString(new Date(lxmfMessage.created_at))}`);
}
lines.push(`Method: ${lxmfMessage.method ?? "unknown"}`);
// add audio attachment size
if(lxmfMessage.fields?.audio?.audio_bytes){
const audioBytesLength = atob(lxmfMessage.fields?.audio?.audio_bytes).length;
info.push(`Audio Attachment: ${this.formatBytes(audioBytesLength)}`);
lines.push(`Audio Attachment: ${this.formatBytes(audioBytesLength)}`);
}
// add image attachment size
if(lxmfMessage.fields?.image?.image_bytes){
const imageBytesLength = atob(lxmfMessage.fields?.image?.image_bytes).length;
info.push(`Image Attachment: ${this.formatBytes(imageBytesLength)}`);
lines.push(`Image Attachment: ${this.formatBytes(imageBytesLength)}`);
}
// add file attachments size
if(lxmfMessage.fields?.file_attachments){
var filesLength = 0;
for(const fileAttachment of lxmfMessage.fields?.file_attachments){
const fileBytesLength = atob(fileAttachment.file_bytes).length;
filesLength += fileBytesLength;
}
info.push(`File Attachments: ${this.formatBytes(filesLength)}`);
lines.push(`File Attachments: ${this.formatBytes(filesLength)}`);
}
// add signal quality if available
if(lxmfMessage.quality != null){
info.push(`Signal Quality: ${lxmfMessage.quality}%`);
if(!isOutbound){
if(lxmfMessage.quality != null){
lines.push(`Signal Quality: ${lxmfMessage.quality}%`);
}
if(lxmfMessage.rssi != null){
lines.push(`RSSI: ${lxmfMessage.rssi}dBm`);
}
if(lxmfMessage.snr != null){
lines.push(`SNR: ${lxmfMessage.snr}dB`);
}
}
// add rssi if available
if(lxmfMessage.rssi != null){
info.push(`RSSI: ${lxmfMessage.rssi}dBm`);
}
// add snr if available
if(lxmfMessage.snr != null){
info.push(`SNR: ${lxmfMessage.snr}dB`);
}
// show message info
DialogUtils.alert(info.join("\n"));
return lines;
},
},
computed: {
@@ -1621,7 +1709,7 @@ export default {
<style scoped>
.attachment-card {
@apply relative flex gap-3 border border-gray-200 bg-white/80 dark:border-zinc-800 dark:bg-zinc-900/70 rounded-2xl p-3 shadow-sm;
@apply relative flex gap-3 border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 rounded-2xl p-3 shadow-sm;
}
.attachment-card__preview {
@apply w-24 h-24 overflow-hidden rounded-xl bg-gray-100 dark:bg-zinc-800 cursor-pointer;
@@ -1639,7 +1727,7 @@ export default {
@apply absolute top-2 right-2 inline-flex items-center justify-center w-6 h-6 rounded-full bg-gray-200 dark:bg-zinc-800 text-gray-600 dark:text-gray-200 hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/40;
}
.attachment-chip {
@apply flex items-center justify-between gap-2 border border-gray-200 dark:border-zinc-800 bg-white/80 dark:bg-zinc-900/70 rounded-full px-3 py-1 text-xs shadow-sm;
@apply flex items-center justify-between gap-2 border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 rounded-full px-3 py-1 text-xs shadow-sm;
}
.attachment-chip__remove {
@apply inline-flex items-center justify-center text-gray-500 dark:text-gray-300 hover:text-red-500;
@@ -1647,4 +1735,20 @@ export default {
.attachment-action-button {
@apply inline-flex items-center gap-1 rounded-full border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/80 px-3 py-1.5 text-xs font-semibold text-gray-800 dark:text-gray-100 shadow-sm hover:border-blue-400 dark:hover:border-blue-500 transition;
}
.audio-controls-light {
filter: invert(1) hue-rotate(180deg);
}
.dark .audio-controls-light {
filter: none;
}
.audio-controls-dark {
filter: none;
}
.dark .audio-controls-dark {
filter: invert(1) hue-rotate(180deg);
}
</style>

View File

@@ -103,37 +103,36 @@ export default {
},
methods: {
async onComposeNewMessage(destinationHash) {
// ask for destination address if not provided
if(destinationHash == null){
destinationHash = await DialogUtils.prompt("Enter LXMF Address");
if(!destinationHash){
if(this.selectedPeer){
return;
}
this.$nextTick(() => {
const composeInput = document.getElementById("compose-input");
if(composeInput){
composeInput.focus();
}
});
return;
}
// if user provided an address with an "lxmf@" prefix, lets remove that to get the raw destination hash
if(destinationHash.startsWith("lxmf@")){
destinationHash = destinationHash.replace("lxmf@", "");
}
// fetch updated announce as we might be composing new message before we loaded the announces list
await this.getLxmfDeliveryAnnounce(destinationHash);
// attempt to find existing peer so we can show their name
const existingPeer = this.peers[destinationHash];
if(existingPeer){
this.onPeerClick(existingPeer);
return;
}
// simple attempt to prevent garbage input
if(destinationHash.length !== 32){
DialogUtils.alert("Invalid Address");
return;
}
// we didn't find an existing peer, so just use an unknown name
this.onPeerClick({
display_name: "Unknown Peer",
destination_hash: destinationHash,

View File

@@ -44,12 +44,12 @@
<MaterialDesignIcon icon-name="account-outline" class="w-6 h-6"/>
</div>
</div>
<div class="mr-auto w-full pr-2">
<div class="flex justify-between gap-2">
<div class="text-gray-900 dark:text-gray-100 truncate" :title="conversation.custom_display_name ?? conversation.display_name" :class="{ 'font-semibold': conversation.is_unread || conversation.failed_messages_count > 0 }">
<div class="mr-auto w-full pr-2 min-w-0">
<div class="flex justify-between gap-2 min-w-0">
<div class="text-gray-900 dark:text-gray-100 truncate min-w-0" :title="conversation.custom_display_name ?? conversation.display_name" :class="{ 'font-semibold': conversation.is_unread || conversation.failed_messages_count > 0 }">
{{ conversation.custom_display_name ?? conversation.display_name }}
</div>
<div class="text-gray-500 dark:text-gray-400 text-xs whitespace-nowrap">
<div class="text-gray-500 dark:text-gray-400 text-xs whitespace-nowrap flex-shrink-0">
{{ formatTimeAgo(conversation.updated_at) }}
</div>
</div>
@@ -128,7 +128,7 @@
<MaterialDesignIcon icon-name="account-outline" class="w-6 h-6"/>
</div>
</div>
<div>
<div class="min-w-0 flex-1">
<div class="text-gray-900 dark:text-gray-100 truncate" :title="peer.custom_display_name ?? peer.display_name">{{ peer.custom_display_name ?? peer.display_name }}</div>
<div class="flex space-x-1 text-gray-500 dark:text-gray-400 text-sm">

View File

@@ -1,19 +1,28 @@
<template>
<div class="inline-flex rounded-md shadow-sm">
<div class="relative inline-flex rounded-xl shadow-sm">
<!-- send button -->
<button @click="send" :disabled="!canSendMessage" type="button" class="my-auto inline-flex items-center rounded-l-md px-2.5 py-1.5 text-sm font-semibold text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2" :class="[ canSendMessage ? 'bg-blue-500 dark:bg-blue-600 hover:bg-blue-400 dark:hover:bg-blue-500 focus-visible:outline-blue-500 dark:focus-visible:outline-blue-600' : 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 dark:focus-visible:outline-zinc-500 cursor-not-allowed']">
<span v-if="isSendingMessage">Sending...</span>
<span v-else class="space-x-1">
<span>Send</span>
<span v-if="deliveryMethod === 'direct'">(Direct Link)</span>
<span v-if="deliveryMethod === 'opportunistic'">(Opportunistic)</span>
<span v-if="deliveryMethod === 'propagated'">(Propagated)</span>
<button @click="send" :disabled="!canSendMessage" type="button" class="inline-flex items-center gap-2 rounded-l-xl px-4 py-2 text-sm font-semibold text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2" :class="[ canSendMessage ? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500' : 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 cursor-not-allowed']">
<svg v-if="!isSendingMessage" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
</svg>
<span v-if="isSendingMessage" class="flex items-center gap-2">
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</span>
<span v-else>
<span v-if="deliveryMethod === 'direct'">Send (Direct)</span>
<span v-else-if="deliveryMethod === 'opportunistic'">Send (Opportunistic)</span>
<span v-else-if="deliveryMethod === 'propagated'">Send (Propagated)</span>
<span v-else>Send</span>
</span>
</button>
<div class="relative">
<!-- dropdown button -->
<button @click="showMenu" :disabled="!canSendMessage" type="button" class="my-auto border-l relative inline-flex items-center rounded-r-md px-2 py-1.5 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2" :class="[ canSendMessage ? 'bg-blue-500 dark:bg-blue-600 hover:bg-blue-400 dark:hover:bg-blue-500 focus-visible:outline-blue-500 dark:focus-visible:outline-blue-600 border-blue-600 dark:border-blue-700' : 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 dark:focus-visible:outline-zinc-500 border-gray-500 dark:border-zinc-600 cursor-not-allowed']">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
<button @click="showMenu" :disabled="!canSendMessage" type="button" class="border-l relative inline-flex items-center justify-center rounded-r-xl px-2.5 py-2 text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2" :class="[ canSendMessage ? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500 border-blue-700 dark:border-blue-800' : 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 border-gray-500 dark:border-zinc-600 cursor-not-allowed']">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
</svg>
</button>
@@ -25,12 +34,12 @@
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95">
<div v-if="isShowingMenu" v-click-outside="hideMenu" class="absolute bottom-0 -ml-11 right-0 ml-0 z-10 mb-10 rounded-md bg-white dark:bg-zinc-800 shadow-lg ring-1 ring-black dark:ring-zinc-700 ring-opacity-5 focus:outline-none">
<div v-if="isShowingMenu" v-click-outside="hideMenu" class="absolute bottom-full right-0 mb-1 z-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none overflow-hidden min-w-[200px]">
<div class="py-1">
<button @click="setDeliveryMethod(null)" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-700 whitespace-nowrap border-b border-gray-200 dark:border-zinc-700">Send Automatically</button>
<button @click="setDeliveryMethod('direct')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-700 whitespace-nowrap">Send over Direct Link</button>
<button @click="setDeliveryMethod('opportunistic')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-700 whitespace-nowrap">Send Opportunistically</button>
<button @click="setDeliveryMethod('propagated')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-700 whitespace-nowrap">Send to Propagation Node</button>
<button @click="setDeliveryMethod(null)" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap border-b border-gray-100 dark:border-zinc-800">Send Automatically</button>
<button @click="setDeliveryMethod('direct')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap">Send over Direct Link</button>
<button @click="setDeliveryMethod('opportunistic')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap">Send Opportunistically</button>
<button @click="setDeliveryMethod('propagated')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap">Send to Propagation Node</button>
</div>
</div>
</Transition>

View File

@@ -40,7 +40,7 @@
</div>
<DropDownMenu>
<template #button>
<IconButton class="bg-transparent dark:bg-transparent">
<IconButton class="bg-transparent dark:bg-transparent w-8 h-8 p-0 flex items-center justify-center">
<MaterialDesignIcon icon-name="dots-vertical" class="w-5 h-5"/>
</IconButton>
</template>