Implement conversation view!
This has a few hacks and is a bit buggy, but at least it's usable and you can begin texting people now :D
This commit is contained in:
parent
db067d14fb
commit
d671932569
|
@ -37,6 +37,13 @@
|
|||
--button-background-focused: #873eff;
|
||||
--button-color-focused: #ffffff;
|
||||
|
||||
--chatbox-indicator-background-focused: #6a6a6a;
|
||||
--chatbox-indicator-color-focused: #ffffff;
|
||||
--chatbox-background: #cccccc;
|
||||
--chatbox-background-primary: #323232;
|
||||
--chatbox-color: #323232;
|
||||
--chatbox-color-primary: #ffffff;
|
||||
|
||||
--checkbox-color: #873eff;
|
||||
--checkbox-color-focused: #ffffff;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import Home from './routes/Home.svelte'
|
||||
import Login from './routes/Login.svelte'
|
||||
import Messages from './routes/Messages.svelte'
|
||||
import Chat from './routes/Chat.svelte'
|
||||
import Redirect from './routes/Redirect.svelte'
|
||||
|
||||
import { titleStore, softkeysStore } from './stores.ts'
|
||||
|
@ -21,6 +22,7 @@
|
|||
'/': Home,
|
||||
'/login': Login,
|
||||
'/messages': Messages,
|
||||
'/chat/:chatID': Chat,
|
||||
'*': Redirect,
|
||||
}
|
||||
|
||||
|
|
86
src/components/ChatMessage.svelte
Normal file
86
src/components/ChatMessage.svelte
Normal file
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
export let message
|
||||
export let tabindex = -1
|
||||
export let autofocus = false
|
||||
export let onclick = () => {}
|
||||
</script>
|
||||
|
||||
{#if message}
|
||||
<div
|
||||
class="chat-item-indicator focusable"
|
||||
{tabindex}
|
||||
{autofocus}
|
||||
on:click={onclick}
|
||||
>
|
||||
<div class="chat-item-row">
|
||||
<div class="avatar"></div>
|
||||
<div class="chat-item">
|
||||
<p>{message.attributes?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="chat-item-info">
|
||||
{message.attributes?.nick || message.attributes?.nickname || message.attributes?.from}
|
||||
{new Date(message.attributes?.time).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.chat-item-indicator {
|
||||
background: var(--item-background);
|
||||
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-item-row {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
margin-right: 0.5rem;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
background: var(--chatbox-background);
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
background: var(--chatbox-background);
|
||||
color: var(--chatbox-color);
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
width: calc(100% - 4rem);
|
||||
border-radius: 0 1rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.chat-item p{
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
font-size: 1.7rem;
|
||||
font-weight: 400;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.chat-item-indicator:focus-within {
|
||||
background: var(--chatbox-indicator-background-focused);
|
||||
}
|
||||
|
||||
.chat-item-info {
|
||||
margin-left: 4.5rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: var(--chatbox-color);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.chat-item-indicator:focus-within .chat-item-info {
|
||||
color: var(--chatbox-indicator-color-focused);
|
||||
}
|
||||
</style>
|
|
@ -4,14 +4,16 @@
|
|||
export let hidden = false
|
||||
export let autofocus = false
|
||||
export let value = null
|
||||
export let onfocus = () => {}
|
||||
export let onblur = () => {}
|
||||
</script>
|
||||
|
||||
<div class="input-container">
|
||||
<label class="input-container__label">{label}</label>
|
||||
{#if hidden}
|
||||
<input type="password" tabindex="{tabindex}" class="input-container__input focusable" bind:value={value} {autofocus}/>
|
||||
<input type="password" tabindex="{tabindex}" class="input-container__input focusable" bind:value={value} {autofocus} on:focus={onfocus} on:blur={onblur}/>
|
||||
{:else}
|
||||
<input type="text" tabindex="{tabindex}" class="input-container__input focusable" bind:value={value} {autofocus}/>
|
||||
<input type="text" tabindex="{tabindex}" class="input-container__input focusable" bind:value={value} {autofocus} on:focus={onfocus} on:blur={onblur}/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,9 +5,15 @@
|
|||
export let tabindex = -1
|
||||
export let autofocus = false
|
||||
export let fullheight = false
|
||||
export let onclick = () => {}
|
||||
</script>
|
||||
|
||||
<div class="list-item-indicator focusable{fullheight ? ' fullheight' : ''}" {tabindex} {autofocus}>
|
||||
<div
|
||||
class="list-item-indicator focusable{fullheight ? ' fullheight' : ''}"
|
||||
{tabindex}
|
||||
{autofocus}
|
||||
on:click={onclick}
|
||||
>
|
||||
{#if text}
|
||||
<p class="list-item-indicator__text">{text}</p>
|
||||
{/if}
|
||||
|
|
|
@ -17,6 +17,8 @@ import "@converse/headless/plugins/smacks/index.js"; // XEP-0198 Stream Mana
|
|||
import "@converse/headless/plugins/status/index.js";
|
||||
import "@converse/headless/plugins/vcard/index.js"; // XEP-0054 VCard-temp
|
||||
|
||||
import "@converse/headless/plugins/emojis/index.js"; // Emojis
|
||||
|
||||
// We're just importing these to activate the addon
|
||||
import {
|
||||
convertASCII2Emoji,
|
||||
|
@ -32,6 +34,7 @@ import {
|
|||
hexToArrayBuffer,
|
||||
stringToArrayBuffer
|
||||
} from '@converse/headless/utils/arraybuffer.js';
|
||||
import { getHyperlinkTemplate } from '@converse/headless/utils/html.js'
|
||||
|
||||
|
||||
window.converse = converse;
|
||||
|
@ -77,6 +80,10 @@ converse.plugins.add('convo', {
|
|||
//_converse.api.listen.on('message', m => console.log('message', m));
|
||||
|
||||
console.debug('Handlers ready!')
|
||||
|
||||
// emoji don't seem to be getting initialized,
|
||||
// so let's do it manually
|
||||
_converse.api.emojis.initialize()
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
154
src/routes/Chat.svelte
Normal file
154
src/routes/Chat.svelte
Normal file
|
@ -0,0 +1,154 @@
|
|||
<script lang="ts">
|
||||
import Text from '../components/Text.svelte'
|
||||
import ChatMessage from '../components/ChatMessage.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 = 'Send'
|
||||
let chatbox
|
||||
let messages
|
||||
let chatListEl
|
||||
let composeBox
|
||||
|
||||
$: title = chatbox?.attributes?.nickname || chatbox?.attributes?.id || params.chatID || 'Convo'
|
||||
$: titleStore.update(() => title)
|
||||
|
||||
function scrollToLatest() {
|
||||
let messageListEl = document.querySelector('.message-list')
|
||||
if (messageListEl) {
|
||||
messageListEl.scrollTo(0, messageListEl.scrollHeight)
|
||||
}
|
||||
}
|
||||
|
||||
softkeysStore.update((k) => {
|
||||
k.left.label = 'Back'
|
||||
k.left.callback = () => { push('/messages') }
|
||||
|
||||
k.center.label = 'Enter'
|
||||
k.center.callback = async () => {
|
||||
let el = document.querySelector('.compose-box input')
|
||||
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 messsage list
|
||||
messages = [...chatbox.messages]
|
||||
|
||||
// Scroll to the latest chat
|
||||
setTimeout(scrollToLatest, 500)
|
||||
}
|
||||
} else {
|
||||
// focus the input element
|
||||
el.focus()
|
||||
}
|
||||
}
|
||||
|
||||
k.right.label = 'Options'
|
||||
k.right.callback = () => {}
|
||||
|
||||
return k
|
||||
})
|
||||
|
||||
$: softkeysStore.update((k) => {
|
||||
k.center.label = centerSoftkeyLabel
|
||||
|
||||
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)
|
||||
|
||||
function onComposeBoxFocus() {
|
||||
// scroll to last message, for convenience
|
||||
scrollToLatest()
|
||||
|
||||
centerSoftkeyLabel = 'Send'
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
// Scroll down if we're on the compose box
|
||||
// TODO: only do this if it's for the current chat
|
||||
let el = document.querySelector('.compose-box input')
|
||||
if(document.activeElement == el) {
|
||||
scrollToLatest()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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}/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="compose-box">
|
||||
<Input
|
||||
bind:value={composeBox}
|
||||
tabindex={chatbox?.messages?.length || 0}
|
||||
autofocus={true}
|
||||
label='Compose'
|
||||
onfocus={onComposeBoxFocus}
|
||||
onblur={() => { centerSoftkeyLabel = 'Compose' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
|
@ -18,7 +18,9 @@
|
|||
k.left.callback = () => {}
|
||||
|
||||
k.center.label = 'Open'
|
||||
k.center.callback = () => {}
|
||||
k.center.callback = () => {
|
||||
document.activeElement.click()
|
||||
}
|
||||
|
||||
k.right.label = 'Options'
|
||||
k.right.callback = () => {}
|
||||
|
@ -26,6 +28,11 @@
|
|||
return k
|
||||
})
|
||||
|
||||
// Main function for this view: get a conversation going!
|
||||
function openChat(chatID) {
|
||||
push(`/chat/${chatID}`)
|
||||
}
|
||||
|
||||
// Run this and unsubscribe after one
|
||||
// second. (The xmppConnected.subscribe function
|
||||
// returns the unsubscribe callback, which we
|
||||
|
@ -36,7 +43,7 @@
|
|||
}
|
||||
}), 1000)
|
||||
|
||||
let chatboxes = []
|
||||
let chatboxes = _converse?.chatboxes?.toArray() || []
|
||||
|
||||
_converse.on('chatBoxesFetched', () => {
|
||||
chatboxes = _converse.chatboxes?.toArray() || []
|
||||
|
@ -52,11 +59,12 @@
|
|||
<p>You have no conversations. How about starting one?</p>
|
||||
</Text>
|
||||
{:else}
|
||||
{#each chatboxes as convo, index}
|
||||
{#each chatboxes as convo, index (convo.id)}
|
||||
<ListItem
|
||||
text={convo.attributes.nickname || convo.attributes.id}
|
||||
subtext={convo.messages?.last()?.attributes?.body || undefined}
|
||||
tabindex={index}
|
||||
onclick={() => openChat(convo.id)}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
|
Loading…
Reference in a new issue