convo/src/routes/Chat.svelte
Badri Sunderarajan 073176a9c9
Allow the options menu to toggle itself
Earlier, the toggle was only happening from the outside, but now
we can toggle the options menu from the menu itself. This is
useful when we want to close the menu from inside, for example
when an option has been selected or the back button has been
pressed.
2024-03-24 23:10:22 +05:30

273 lines
6.2 KiB
Svelte

<script lang="ts">
import Text from '../components/Text.svelte'
import ChatMessage from '../components/ChatMessage.svelte'
import OptionsMenu from '../components/OptionsMenu.svelte'
import ListItem from '../components/ListItem.svelte'
import Input from '../components/Input.svelte'
import { onMount } from 'svelte'
import { push } from 'svelte-spa-router'
import { converse, _converse } from '@converse/headless/core'
import {
titleStore,
softkeysStore,
xmppConnected,
} from '../stores.ts'
export let params = {}
let title
let centerSoftkeyLabel = 'Enter'
let leftSoftkeyLabel = ' '
let chatbox
let messages
let chatListEl
let composeBox
// Whether the options menu is visible or not
let optionsMenuVisible = false
function toggleOptionsMenu() {
optionsMenuVisible = !optionsMenuVisible
}
$: title = chatbox?.attributes?.nickname || chatbox?.attributes?.id || params.chatID || 'Convo'
$: titleStore.update(() => title)
function scrollToLatest(onlyOnChatbox=false) {
// Skip if necessary when chatbox not selected
if (onlyOnChatbox) {
let el = document.querySelector('.compose-box input')
if(document.activeElement != el) return
}
let messageListEl = document.querySelector('.message-list')
if (messageListEl) {
messageListEl.scrollTo(0, messageListEl.scrollHeight)
}
}
$: {
messages
setTimeout(scrollToLatest, 1000) // delay to make sure it happens
}
softkeysStore.update((k) => {
k.left.label = leftSoftkeyLabel
k.left.callback = async () => {
let el = document.querySelector('.compose-box input')
// Send message if textbox activated
if (document.activeElement == el) {
// Send only if not blank
if (!!composeBox) {
let messageText = composeBox
composeBox = ''
// We can't process these till the emoji are loaded
await converse.emojis.initialized_promise
chatbox.sendMessage({
body: messageText,
})
// update the message list
messages = [...chatbox.messages]
}
}
}
k.center.label = centerSoftkeyLabel
k.center.callback = () => {
// Focus input if not already focused
let el = document.querySelector('.compose-box input')
if (document.activeElement != el) {
// focus the input element
el.focus()
} else {
// Otherwise, we literally type an Enter
if (!composeBox) composeBox = ''
composeBox = composeBox + '\n'
// FIXME: compose box should be a textarea for
// this to actually work.
}
}
k.right.label = 'Options'
k.right.callback = toggleOptionsMenu
return k
})
// Run this and unsubscribe after one
// second. (The xmppConnected.subscribe function
// returns the unsubscribe callback, which we
// then call with setTimeout after 1000 ms)
setTimeout(xmppConnected.subscribe(value => {
if (!value) {
push('/redirect') // redirects to home
}
}), 1000)
// Manage the compose box
function onComposeBoxFocus() {
// scroll to last message, for convenience
scrollToLatest()
centerSoftkeyLabel = 'Enter'
if (!!composeBox) {
leftSoftkeyLabel = 'Send'
} else {
leftSoftkeyLabel = ' '
}
}
function onComposeBoxBlur() {
centerSoftkeyLabel = 'Compose'
leftSoftkeyLabel = ' '
}
function updateCenterSoftkeyLabel() {
softkeysStore.update((k) => {
k.center.label = centerSoftkeyLabel
return k
})
}
function updateLeftSoftkeyLabel() {
softkeysStore.update((k) => {
k.left.label = leftSoftkeyLabel
return k
})
}
function onComposeBoxUpdate() {
/*
* We're assuming the compose box is focused here,
* because how else did it get updated? If this is
* problematic, we can add an `if` condition here
* to check if it is indeed the active element.
* Either way, it only updates the label, not the
* action itself (which is admittedly more or
* less confusing depending on how you look at it).
*/
// Clear 'Send' button if empty
if (leftSoftkeyLabel == 'Send' && !composeBox) {
leftSoftkeyLabel = ' '
// We have to do this since reactivity doesn't recurse
updateLeftSoftkeyLabel()
} else if (leftSoftkeyLabel == ' ' && !!composeBox) {
leftSoftkeyLabel = 'Send'
// Again, no recursion in reactivity
updateLeftSoftkeyLabel()
}
}
$: softkeysStore.update((k) => {
k.center.label = centerSoftkeyLabel
return k
})
$: softkeysStore.update((k) => {
k.left.label = leftSoftkeyLabel
return k
})
$: {
!composeBox // whenever this changes
onComposeBoxUpdate()
}
// Function to close a conversation
function closeChat() {
chatbox.close().then(() => {
push('/messages')
})
}
// Get it all running!
onMount(() => {
// Initialise the chatbox
chatbox = _converse.chatboxes.get(params.chatID)
if (chatbox) {
messages = [...chatbox.messages]
_converse.api.listen.on('message', () => {
// TODO: refresh only when the message is
// coming to this particular chatbox
messages = [...chatbox.messages]
})
}
setTimeout(scrollToLatest, 1000)
})
</script>
{#if !chatbox}
<Text>
<p>Empty conversation</p>
</Text>
{:else}
<div class="message-list">
{#each messages as message, index (message.id)}
<ChatMessage
{message}
tabindex={index}
ourJID={_converse.bare_jid}
/>
{/each}
</div>
{/if}
<div class="compose-box">
<Input
bind:value={composeBox}
tabindex={chatbox?.messages?.length || 0}
autofocus={true}
label='Compose'
on:focus={onComposeBoxFocus}
on:blur={onComposeBoxBlur}
/>
</div>
{#if optionsMenuVisible}
<OptionsMenu
title="Options"
toggler={toggleOptionsMenu}
>
<ListItem
text="Close conversation"
onclick={closeChat}
/>
</OptionsMenu>
{/if}
<style>
.compose-box {
height: 8rem;
position: absolute;
bottom: 3rem; /* for the softkey bar */
left: 0;
right: 0;
}
.message-list {
margin-bottom: 9rem;
overflow: scroll;
height: calc(100vh - 13rem);
}
</style>