mirror of
https://github.com/liamcottle/reticulum-meshchat.git
synced 2026-04-27 16:10:32 +00:00
implement drop down menu for conversations
This commit is contained in:
parent
226384d190
commit
6f4084451b
5 changed files with 183 additions and 28 deletions
92
src/frontend/components/DropDownMenu.vue
Normal file
92
src/frontend/components/DropDownMenu.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<div v-click-outside="{ handler: onClickOutsideMenu, capture: true }" class="cursor-default relative inline-block text-left">
|
||||
|
||||
<!-- menu button -->
|
||||
<div ref="dropdown-button" @click.stop="toggleMenu">
|
||||
<slot name="button"/>
|
||||
</div>
|
||||
|
||||
<!-- drop down menu -->
|
||||
<Transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
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" @click.stop="hideMenu" class="overflow-hidden absolute right-0 z-10 mr-4 w-56 rounded-md bg-white shadow-md border border-gray-200 focus:outline-none" :class="[ dropdownClass ]">
|
||||
<slot name="items"/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DropDownMenu',
|
||||
data() {
|
||||
return {
|
||||
isShowingMenu: false,
|
||||
dropdownClass: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleMenu() {
|
||||
if(this.isShowingMenu){
|
||||
this.hideMenu();
|
||||
} else {
|
||||
this.showMenu();
|
||||
}
|
||||
},
|
||||
showMenu() {
|
||||
this.isShowingMenu = true;
|
||||
this.adjustDropdownPosition();
|
||||
},
|
||||
hideMenu() {
|
||||
this.isShowingMenu = false;
|
||||
},
|
||||
onClickOutsideMenu(event) {
|
||||
if(this.isShowingMenu){
|
||||
event.preventDefault();
|
||||
this.hideMenu();
|
||||
}
|
||||
},
|
||||
adjustDropdownPosition() {
|
||||
this.$nextTick(() => {
|
||||
|
||||
// find button and dropdown
|
||||
const button = this.$refs["dropdown-button"];
|
||||
const dropdown = button.nextElementSibling;
|
||||
|
||||
// do nothing if not found
|
||||
if(!button || !dropdown){
|
||||
return;
|
||||
}
|
||||
|
||||
// get bounding box of button and dropdown
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const dropdownRect = dropdown.getBoundingClientRect();
|
||||
|
||||
// calculate how much space is under and above the button
|
||||
const spaceBelowButton = window.innerHeight - buttonRect.bottom;
|
||||
const spaceAboveButton = buttonRect.top;
|
||||
|
||||
// calculate if there is enough space available to show dropdown
|
||||
const hasEnoughSpaceAboveButton = spaceAboveButton > dropdownRect.height;
|
||||
const hasEnoughSpaceBelowButton = spaceBelowButton > dropdownRect.height;
|
||||
|
||||
// show dropdown above button
|
||||
if(hasEnoughSpaceAboveButton && !hasEnoughSpaceBelowButton){
|
||||
this.dropdownClass = "bottom-0 mb-12";
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise fallback to showing dropdown below button
|
||||
this.dropdownClass = "top-0 mt-12";
|
||||
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
11
src/frontend/components/DropDownMenuItem.vue
Normal file
11
src/frontend/components/DropDownMenuItem.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div class="cursor-pointer flex p-3 space-x-2 text-sm text-gray-500 hover:bg-gray-100">
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DropDownMenuItem',
|
||||
}
|
||||
</script>
|
||||
11
src/frontend/components/IconButton.vue
Normal file
11
src/frontend/components/IconButton.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<button type="button" class="p-2 rounded-full text-gray-700 bg-gray-100 hover:bg-gray-200">
|
||||
<slot/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'IconButton',
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<DropDownMenu>
|
||||
<template v-slot:button>
|
||||
<IconButton>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
</template>
|
||||
<template v-slot:items>
|
||||
|
||||
<!-- delete message history button -->
|
||||
<DropDownMenuItem @click="onDeleteMessageHistory">
|
||||
<svg class="size-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 0 0 6 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 1 0 .23 1.482l.149-.022.841 10.518A2.75 2.75 0 0 0 7.596 19h4.807a2.75 2.75 0 0 0 2.742-2.53l.841-10.52.149.023a.75.75 0 0 0 .23-1.482A41.03 41.03 0 0 0 14 4.193V3.75A2.75 2.75 0 0 0 11.25 1h-2.5ZM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4ZM8.58 7.72a.75.75 0 0 0-1.5.06l.3 7.5a.75.75 0 1 0 1.5-.06l-.3-7.5Zm4.34.06a.75.75 0 1 0-1.5-.06l-.3 7.5a.75.75 0 1 0 1.5.06l.3-7.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="text-red-500">Delete Message History</span>
|
||||
</DropDownMenuItem>
|
||||
|
||||
</template>
|
||||
</DropDownMenu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DropDownMenu from "../DropDownMenu.vue";
|
||||
import DropDownMenuItem from "../DropDownMenuItem.vue";
|
||||
import IconButton from "../IconButton.vue";
|
||||
import DialogUtils from "../../js/DialogUtils";
|
||||
|
||||
export default {
|
||||
name: 'ConversationDropDownMenu',
|
||||
components: {
|
||||
IconButton,
|
||||
DropDownMenuItem,
|
||||
DropDownMenu,
|
||||
},
|
||||
props: {
|
||||
peer: Object,
|
||||
},
|
||||
emits: [
|
||||
"conversation-deleted",
|
||||
],
|
||||
methods: {
|
||||
async onDeleteMessageHistory() {
|
||||
|
||||
// ask user to confirm deleting conversation history
|
||||
if(!confirm("Are you sure you want to delete all messages in this conversation? This can not be undone!")){
|
||||
return;
|
||||
}
|
||||
|
||||
// delete all lxmf messages from "us to destination" and from "destination to us"
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/lxmf-messages/conversation/${this.peer.destination_hash}`);
|
||||
} catch(e) {
|
||||
DialogUtils.alert("failed to delete conversation");
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// fire callback
|
||||
this.$emit("conversation-deleted");
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -76,15 +76,7 @@
|
|||
|
||||
<!-- delete button -->
|
||||
<div class="my-auto mr-2">
|
||||
<div @click="deleteConversation" class="cursor-pointer">
|
||||
<div class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConversationDropDownMenu v-if="selectedPeer" :peer="selectedPeer" @conversation-deleted="onConversationDeleted"/>
|
||||
</div>
|
||||
|
||||
<!-- close button -->
|
||||
|
|
@ -414,10 +406,12 @@ import AddAudioButton from "./AddAudioButton.vue";
|
|||
import moment from "moment";
|
||||
import SendMessageButton from "./SendMessageButton.vue";
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import ConversationDropDownMenu from "./ConversationDropDownMenu.vue";
|
||||
|
||||
export default {
|
||||
name: 'ConversationViewer',
|
||||
components: {
|
||||
ConversationDropDownMenu,
|
||||
MaterialDesignIcon,
|
||||
SendMessageButton,
|
||||
AddAudioButton,
|
||||
|
|
@ -844,25 +838,7 @@ export default {
|
|||
}
|
||||
|
||||
},
|
||||
async deleteConversation() {
|
||||
|
||||
// do nothing if no peer selected
|
||||
if(!this.selectedPeer){
|
||||
return;
|
||||
}
|
||||
|
||||
// ask user to confirm deleting conversation history
|
||||
if(!confirm("Are you sure you want to delete all messages from this conversation? This can not be undone!")){
|
||||
return;
|
||||
}
|
||||
|
||||
// delete all lxmf messages from "us to destination" and from "destination to us"
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/lxmf-messages/conversation/${this.selectedPeer.destination_hash}`);
|
||||
} catch(e) {
|
||||
DialogUtils.alert("failed to delete conversation");
|
||||
console.log(e);
|
||||
}
|
||||
async onConversationDeleted() {
|
||||
|
||||
// reload conversation
|
||||
await this.initialLoad();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue