375 lines
No EOL
12 KiB
Svelte
375 lines
No EOL
12 KiB
Svelte
<script>
|
|
import { chatAdapter, chatData, selectedMessage, zapsPerMessage } from './lib/store';
|
|
import { onMount } from 'svelte';
|
|
import NostrNote from './NostrNote.svelte';
|
|
import * as animateScroll from "svelte-scrollto";
|
|
|
|
let events = [];
|
|
let responseEvents = [];
|
|
let responses = {};
|
|
let profiles = {};
|
|
|
|
export let websiteOwnerPubkey;
|
|
export let chatConfiguration;
|
|
let prevChatConfiguration;
|
|
|
|
$: {
|
|
if (chatConfiguration !== prevChatConfiguration && $chatAdapter) {
|
|
$chatAdapter.setChatConfiguration(
|
|
chatConfiguration.chatType,
|
|
chatConfiguration.chatTags,
|
|
chatConfiguration.chatReferenceTags,
|
|
chatConfiguration.chatId);
|
|
events = [];
|
|
responses = {};
|
|
rootNoteId = null;
|
|
localStorage.removeItem('rootNoteId');
|
|
|
|
// rootNoteId = localStorage.getItem('rootNoteId');
|
|
// if (rootNoteId) {
|
|
// $chatAdapter.subscribeToEventAndResponses(rootNoteId);
|
|
// }
|
|
}
|
|
prevChatConfiguration = chatConfiguration;
|
|
}
|
|
|
|
function getEventById(eventId) {
|
|
let event = events.find(e => e.id === eventId);
|
|
event = event || responseEvents.find(e => e.id === eventId);
|
|
return event;
|
|
}
|
|
|
|
async function sendMessage() {
|
|
const input = document.getElementById('message-input');
|
|
const message = input.value;
|
|
input.value = '';
|
|
let extraParams = { tags: [], tagPubKeys: [] };
|
|
|
|
// if this is the rootLevel we want to tag the owner of the site's pubkey
|
|
if (!rootNoteId && websiteOwnerPubkey) { extraParams.tagPubKeys = [websiteOwnerPubkey] }
|
|
|
|
// if we are responding to an event, we want to tag the event and the pubkey
|
|
if ($selectedMessage) {
|
|
extraParams.tags.push(['e', $selectedMessage, "wss://nos.lol", "root"]);
|
|
extraParams.tagPubKeys.push(getEventById($selectedMessage).pubkey);
|
|
}
|
|
|
|
// if (rootNoteId) {
|
|
// // mark it as a response to the most recent event
|
|
// const mostRecentEvent = events[events.length - 1];
|
|
// // go through all the tags and add them to the new message
|
|
// if (mostRecentEvent) {
|
|
// mostRecentEvent.tags.forEach(tag => {
|
|
// if (tag[0] === 'e') {
|
|
// extraParams.tags.push(tag);
|
|
// }
|
|
// })
|
|
// extraParams.tags.push(['e', mostRecentEvent.id]);
|
|
// extraParams.tags.push(['p', mostRecentEvent.pubkey]);
|
|
// }
|
|
// }
|
|
|
|
const noteId = await $chatAdapter.send(message, extraParams);
|
|
|
|
if (!rootNoteId) {
|
|
rootNoteId = noteId;
|
|
localStorage.setItem('rootNoteId', rootNoteId);
|
|
}
|
|
}
|
|
|
|
async function inputKeyDown(event) {
|
|
if (event.key === 'Enter') {
|
|
sendMessage();
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
function messageReceived(message) {
|
|
const messageLastEventTag = message.tags.filter(tag => tag[0] === 'e').pop();
|
|
let isThread;
|
|
|
|
if (chatConfiguration.chatType === 'GLOBAL') {
|
|
isThread = message.tags.filter(tag => tag[0] === 'e').length >= 1;
|
|
} else if (chatConfiguration.chatType === 'GROUP') {
|
|
isThread = message.tags.filter(tag => tag[0] === 'e' && tag[1] !== chatConfiguration.chatId).length >= 1;
|
|
} else {
|
|
const pubkeysTagged = message.tags.filter(tag => tag[0] === 'p').map(tag => tag[1]);
|
|
isThread = new Set(pubkeysTagged).size >= 2;
|
|
}
|
|
|
|
if (!responses[message.id]) { responses[message.id] = [] };
|
|
|
|
if (isThread) {
|
|
// get the last "e" tag, which is tagging the immediate parent
|
|
const lastETag = message.tags.filter(tag => tag[0] === 'e').pop();
|
|
if (lastETag && lastETag[1]) {
|
|
// if there is one, add it to the response
|
|
if (!responses[lastETag[1]]) {
|
|
responses[lastETag[1]] = [];
|
|
}
|
|
responses[lastETag[1]].push(message);
|
|
}
|
|
|
|
responseEvents.push(message);
|
|
responseEvents = responseEvents;
|
|
} else {
|
|
// insert message so that it's chronologically ordered by created_at
|
|
let index = 0;
|
|
while (index < events.length && events[index].created_at < message.created_at) {
|
|
index++;
|
|
}
|
|
events.splice(index, 0, message);
|
|
events = events;
|
|
}
|
|
|
|
responses = responses;
|
|
|
|
scrollDown()
|
|
}
|
|
|
|
function scrollDown() {
|
|
animateScroll.scrollToBottom({
|
|
container: document.getElementById('messages-container'),
|
|
offset: 999999, // hack, oh well, browsers suck
|
|
duration: 50
|
|
})
|
|
}
|
|
|
|
function zapReceived(zap) {
|
|
const event = events.find(event => event.id === zap.zappedEvent);
|
|
if (!event) { return; }
|
|
|
|
if (!$zapsPerMessage[event.id]) $zapsPerMessage[event.id] = [];
|
|
$zapsPerMessage[event.id].push(zap);
|
|
}
|
|
|
|
function reactionReceived(reaction) {
|
|
const event = events.find(event => event.id === reaction.id);
|
|
if (!event) { return; }
|
|
|
|
event.reactions = event.reactions || [];
|
|
event.reactions.push(reaction);
|
|
events = events;
|
|
}
|
|
|
|
let rootNoteId;
|
|
let channelMetadata = {};
|
|
|
|
onMount(() => {
|
|
$chatAdapter.on('message', messageReceived);
|
|
|
|
$chatAdapter.on('connectivity', (e) => {
|
|
connectivityStatus = e;
|
|
})
|
|
|
|
$chatAdapter.on('reaction', reactionReceived);
|
|
$chatAdapter.on('zap', zapReceived);
|
|
$chatAdapter.on('deleted', (deletedEvents) => {
|
|
deletedEvents.forEach(deletedEventId => {
|
|
const index = events.findIndex(event => event.id === deletedEventId);
|
|
if (index !== -1) {
|
|
events[index].deleted = true;
|
|
events = events;
|
|
}
|
|
})
|
|
});
|
|
|
|
$chatAdapter.on('profile', ({pubkey, profile}) => {
|
|
let profiles = $chatData.profiles;
|
|
profiles[pubkey] = profile;
|
|
|
|
chatData.set({ profiles, ...$chatData })
|
|
})
|
|
|
|
$chatAdapter.on('channelMetadata', (event) => {
|
|
channelMetadata = JSON.parse(event.content);
|
|
})
|
|
});
|
|
|
|
let connectivityStatus = {};
|
|
let connectedRelays = 0;
|
|
let totalRelays = 0;
|
|
|
|
$: {
|
|
connectedRelays = Object.values(connectivityStatus).filter(status => status === 'connected').length;
|
|
totalRelays = Object.values(connectivityStatus).length;
|
|
|
|
if ($chatAdapter?.pubkey && !profiles[$chatAdapter.pubkey]) {
|
|
$chatAdapter.reqProfile($chatAdapter.pubkey)
|
|
}
|
|
}
|
|
|
|
let connectedChatId;
|
|
|
|
$: if (connectedChatId !== $chatAdapter?.chatId) {
|
|
connectedChatId = $chatAdapter?.chatId;
|
|
channelMetadata = {};
|
|
}
|
|
|
|
$: profiles = $chatData.profiles;
|
|
|
|
function selectParent() {
|
|
if (chatConfiguration.chatType === 'GROUP') {
|
|
$selectedMessage = null;
|
|
} else {
|
|
// get the last tagged event in the tags array of the current $selectedMessage
|
|
const lastETag = getEventById($selectedMessage).tags.filter(tag => tag[0] === 'e').pop();
|
|
const lastETagId = lastETag && lastETag[1];
|
|
|
|
$selectedMessage = lastETagId;
|
|
}
|
|
|
|
scrollDown()
|
|
}
|
|
|
|
let ownName;
|
|
$: ownName = $chatAdapter?.pubkey ? pubkeyName($chatAdapter.pubkey) : "";
|
|
|
|
function pubkeyName(pubkey) {
|
|
let name;
|
|
|
|
if (profiles[$chatAdapter.pubkey]) {
|
|
let self = profiles[$chatAdapter.pubkey];
|
|
|
|
// https://xkcd.com/927/
|
|
name = self.display_name ||
|
|
self.displayName ||
|
|
self.name ||
|
|
self.nip05;
|
|
|
|
}
|
|
|
|
if (!name) { name = `[${pubkey.slice(0, 6)}]`; }
|
|
|
|
return name;
|
|
}
|
|
|
|
</script>
|
|
|
|
<div class="
|
|
bg-purple-700 text-white
|
|
-mx-4 -mt-5 mb-3
|
|
px-4 py-3
|
|
overflow-clip
|
|
flex flex-row justify-between items-center
|
|
">
|
|
|
|
<div class="text-lg font-semibold">
|
|
{#if $chatAdapter?.pubkey}
|
|
{ownName}
|
|
{/if}
|
|
</div>
|
|
|
|
<span class="text-xs flex flex-col items-end mt-2 text-gray-200 gap-1">
|
|
<div class="flex flex-row gap-1 overflow-clip">
|
|
{#each Array(totalRelays) as _, i}
|
|
<span class="
|
|
inline-block
|
|
rounded-full
|
|
{connectedRelays > i ? 'bg-green-500' : 'bg-gray-300'}
|
|
w-2 h-2
|
|
"></span>
|
|
{/each}
|
|
</div>
|
|
|
|
{connectedRelays}/{totalRelays} relays
|
|
</span>
|
|
</div>
|
|
|
|
{#if channelMetadata.name}
|
|
<div class="flex flex-row gap-2 mb-3 bg-zinc-300 text-zinc-800 px-4 py-2 -mx-4 -mt-3">
|
|
{#if channelMetadata.picture}
|
|
<img src={channelMetadata.picture} class="w-12 h-12 rounded-full" />
|
|
{/if}
|
|
|
|
<div class="flex flex-col">
|
|
<div class="font-extrabold text-xl">{channelMetadata.name}</div>
|
|
{#if channelMetadata.about}
|
|
<div class="text-sm truncate font-regular">{channelMetadata.about}</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if $selectedMessage}
|
|
{#if !getEventById($selectedMessage)}
|
|
<h1>Couldn't find event with ID {$selectedMessage}</h1>
|
|
{:else}
|
|
<div class="flex flex-row mb-3">
|
|
<a href='#' on:click|preventDefault={selectParent}>
|
|
<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="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
|
|
</svg>
|
|
</a>
|
|
|
|
<div class="flex flex-col ml-2">
|
|
<span class="text-lg text-black overflow-hidden whitespace-nowrap text-ellipsis">
|
|
{getEventById($selectedMessage).content}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div id="messages-container" class="overflow-auto -mx-4 px-4" style="height: 50vh; min-height: 300px;">
|
|
<div id="messages-container-inner" class="flex flex-col gap-4">
|
|
{#if $selectedMessage}
|
|
<NostrNote event={getEventById($selectedMessage)} {responses} {websiteOwnerPubkey} />
|
|
{:else}
|
|
{#each events as event}
|
|
<NostrNote {event} {responses} {websiteOwnerPubkey} />
|
|
{#if event.deleted}
|
|
👆 deleted
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-col">
|
|
<div class="
|
|
border-y border-y-slate-200
|
|
-mx-4 my-2 bg-slate-100 text-black text-sm
|
|
px-4 py-2
|
|
">
|
|
{#if chatConfiguration.chatType === 'DM'}
|
|
<b>Encrypted chat:</b>
|
|
only your chat partner can see these messages.
|
|
{:else if chatConfiguration.chatType === 'GROUP'}
|
|
<b>Public chat:</b>
|
|
anyone can see these messages.
|
|
{:else}
|
|
<b>Public notes:</b>
|
|
your followers see your messages on their timeline
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex flex-row gap-2 -mx-1">
|
|
<textarea
|
|
type="text"
|
|
id="message-input"
|
|
class="
|
|
-mb-2
|
|
p-2
|
|
w-full
|
|
resize-none
|
|
rounded-xl
|
|
text-gray-600
|
|
border
|
|
" placeholder="Say hello!"
|
|
rows=1
|
|
on:keydown={inputKeyDown}
|
|
></textarea>
|
|
<button type="button" class="inline-flex items-center rounded-full border border-transparent bg-purple-700 p-3 text-white shadow-sm hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" on:click|preventDefault={sendMessage}>
|
|
<!-- Heroicon name: outline/plus -->
|
|
<svg aria-hidden="true" class="w-6 h-6 rotate-90" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
</style> |