summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json2
-rw-r--r--ui/lib/components/ChannelMeta.svelte44
-rw-r--r--ui/routes/(app)/c/[conversation]/+page.svelte15
-rw-r--r--ui/service-worker.js44
-rw-r--r--ui/styles/app-bar.css2
-rw-r--r--ui/styles/forms.css8
-rw-r--r--ui/styles/messages.css24
7 files changed, 138 insertions, 1 deletions
diff --git a/package-lock.json b/package-lock.json
index 849a38c..211ad61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,7 +37,7 @@
"vitest": "^4.0.8"
},
"engines": {
- "node": ">=22.0.0 <23.0.0",
+ "node": ">=22.12.0 <23.0.0",
"npm": ">=10.0.0 <11.0.0"
}
},
diff --git a/ui/lib/components/ChannelMeta.svelte b/ui/lib/components/ChannelMeta.svelte
new file mode 100644
index 0000000..2ee13b6
--- /dev/null
+++ b/ui/lib/components/ChannelMeta.svelte
@@ -0,0 +1,44 @@
+<script>
+ let { vapid, subscription, startExpanded = false, subscribe = async () => null } = $props();
+ let pending = $state(false);
+ let expanded = $state(startExpanded);
+
+ function onsubmit(callback) {
+ return async (evt) => {
+ evt.preventDefault();
+ // Without this, the toggleExpanded function defined below will trigger:
+ evt.stopPropagation();
+
+ pending = true;
+ try {
+ // TODO: is this working?
+ await callback();
+ } finally {
+ pending = false;
+ }
+ };
+ }
+
+ function toggleExpanded() {
+ // TODO: in messages.css we need to slide the whole div up, not leave its
+ // contents static. We need to show three little lines as a handle. We need
+ // to fix colours. We need to keep the meta consistently over the rest of
+ // the message list. We need to fix mobile click-area. Too easy to hit the
+ // title and go to / instead.
+ // Also: we can start expanded optionally, if there is a message we need to
+ // show. This can work for prompts like "activate your notifications".
+ expanded = !expanded;
+ }
+</script>
+
+<div class="channel-meta {expanded ? 'expanded' : ''}" onclick={toggleExpanded}>
+ &nbsp;
+ <div class="inner">
+ {#if !subscription}
+ <form class="form" onsubmit={onsubmit(subscribe)}>
+ <button disabled={pending} type="submit">create push subscription</button>
+ </form>
+ {/if}
+ </div>
+ <div class="handle">&equiv;</div>
+</div>
diff --git a/ui/routes/(app)/c/[conversation]/+page.svelte b/ui/routes/(app)/c/[conversation]/+page.svelte
index 24baa47..be97737 100644
--- a/ui/routes/(app)/c/[conversation]/+page.svelte
+++ b/ui/routes/(app)/c/[conversation]/+page.svelte
@@ -1,6 +1,7 @@
<script>
import { DateTime } from 'luxon';
import { page } from '$app/state';
+ import ChannelMeta from '$lib/components/ChannelMeta.svelte';
import MessageInput from '$lib/components/MessageInput.svelte';
import MessageRun from '$lib/components/MessageRun.svelte';
import Message from '$lib/components/Message.svelte';
@@ -10,6 +11,9 @@
const { session, outbox } = data;
let activeConversation;
+ const subscription = $derived(session.push.subscription);
+ const vapid = $derived(session.push.vapidKey);
+
const conversationId = $derived(page.params.conversation);
const conversation = $derived(
session.conversations.find((conversation) => conversation.id === conversationId),
@@ -56,6 +60,11 @@
if (!!at) {
session.local.updateLastReadAt(conversationId, at);
}
+ navigator.serviceWorker.controller.postMessage({
+ type: 'CONVERSATION_READ',
+ conversationId,
+ at,
+ });
}
$effect(() => {
@@ -82,6 +91,11 @@
lastReadCallback = setTimeout(setLastRead, 2 * 1000);
}
+ async function subscribe() {
+ // TODO: we need to provide specific subscription stuff, right?
+ await session.push.subscribe();
+ }
+
async function sendMessage(message) {
outbox.sendToConversation(conversationId, message);
}
@@ -93,6 +107,7 @@
<svelte:window onkeydown={handleKeydown} />
+<ChannelMeta {subscribe} {vapid} {subscription} />
<div class="active-conversation" {onscroll} bind:this={activeConversation}>
{#each messageRuns as { sender, ownMessage, messages }}
<MessageRun
diff --git a/ui/service-worker.js b/ui/service-worker.js
index cb32d0d..eee3397 100644
--- a/ui/service-worker.js
+++ b/ui/service-worker.js
@@ -53,11 +53,55 @@ self.addEventListener('fetch', (event) => {
event.respondWith(cacheFirst(event.request));
});
+const conversationReadStatus = {
+ // Format:
+ // conversationId: { lastRead: Optional(Datetime), lastMessage: Datetime }
+};
+
+function countUnreadChannels() {
+ return Object.values(conversationReadStatus)
+ .map(({ lastRead, lastMessage }) => {
+ return !lastRead || lastRead < lastMessage ? 1 : 0;
+ })
+ .reduce((total, current) => total + current, 0);
+}
+
self.addEventListener('push', (event) => {
+ // Let's show a notification right away so Safari doesn't tell Apple to be
+ // mad at us:
event.waitUntil(
self.registration.showNotification('Test notification', {
actions: [],
body: event.data.text(),
}),
);
+ // Now we can do slower things that might fail:
+ conversationReadStatus[event.conversationId] ||= { lastRead: null, lastMessage: null };
+ conversationReadStatus[event.conversationId].lastMessage = new Date();
+ event.waitUntil(
+ (async () => {
+ if (navigator.setAppBadge) {
+ navigator.setAppBadge(countUnreadChannels());
+ }
+ })(),
+ );
+});
+
+// The client has to tell us when it has read a conversation:
+self.addEventListener('message', (event) => {
+ switch (event.data?.type) {
+ case 'CONVERSATION_READ':
+ conversationReadStatus[event.data.conversationId] ||= { lastRead: null, lastMessage: null };
+ conversationReadStatus[event.data.conversationId].lastMessage = event.data.at || new Date();
+ event.waitUntil(
+ (async () => {
+ if (navigator.setAppBadge) {
+ navigator.setAppBadge(countUnreadChannels());
+ }
+ })(),
+ );
+ break;
+ default:
+ break;
+ }
});
diff --git a/ui/styles/app-bar.css b/ui/styles/app-bar.css
index ce52712..272810a 100644
--- a/ui/styles/app-bar.css
+++ b/ui/styles/app-bar.css
@@ -7,6 +7,8 @@
align-items: stretch;
background-color: var(--colour-header-bg);
color: var(--light-text);
+ z-index: 2;
+ position: relative;
}
.app-bar > * {
diff --git a/ui/styles/forms.css b/ui/styles/forms.css
index f4d218f..19d31b7 100644
--- a/ui/styles/forms.css
+++ b/ui/styles/forms.css
@@ -26,3 +26,11 @@ form.form > button {
.disabled {
color: var(--light-text);
}
+
+button {
+ cursor: pointer;
+}
+button:active {
+ border-color: red;
+ background-color: blue;
+}
diff --git a/ui/styles/messages.css b/ui/styles/messages.css
index 5488fa5..52355b7 100644
--- a/ui/styles/messages.css
+++ b/ui/styles/messages.css
@@ -1,3 +1,27 @@
+.channel-meta {
+ border-width: 0 1px 1px 1px;
+ border-color: red;
+ border-style: solid;
+ border-radius: 0 0 4px 4px;
+ background-color: mistyrose;
+ overflow: clip;
+ transition: margin-top 0.25s ease-in;
+ z-index: 1;
+ margin-top: -5rem;
+}
+
+.channel-meta.expanded {
+ margin-top: 0rem;
+}
+
+.channel-meta .handle {
+ text-align: center;
+}
+
+.channel-meta .inner {
+ padding: 1rem;
+}
+
.message-run {
border-radius: 0.25rem;
margin-top: 1rem;