Files
plspace/src/routes/Mail.svelte
T

241 lines
8.9 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
/**
* The Mail Center — notifications as an inbox, plus the Friend Request
* Manager with its Approve / Deny buttons.
*
* Folders map onto notification types. The "requests" folder is a different
* endpoint (`/api/v1/follow_requests`) because pending requests aren't
* notifications once they've been read.
*/
import { untrack } from 'svelte'
import type { Account, Notification } from '$lib/api/types'
import { useAppServices } from '$lib/app-services'
import { Feed } from '$lib/stores/feed.svelte'
import { displayNameOf, fullHandle, profilePath } from '$lib/util/profile'
import { toPlainText } from '$lib/util/html'
import { stampDate } from '$lib/util/time'
import Module from '$components/common/Module.svelte'
import Pager from '$components/common/Pager.svelte'
import Avatar from '$components/common/Avatar.svelte'
interface Props {
folder?: string
}
let { folder = 'inbox' }: Props = $props()
const { endpoints, session } = useAppServices()
interface Folder {
key: string
label: string
icon: string
/** Notification types this folder shows; empty means everything. */
types: string[]
}
const FOLDERS: Folder[] = [
{ key: 'inbox', label: 'Inbox', icon: '📥', types: [] },
{ key: 'mentions', label: 'Messages', icon: '✉', types: ['mention'] },
{ key: 'requests', label: 'Friend Requests', icon: '', types: [] },
{ key: 'follows', label: 'New Friends', icon: '👥', types: ['follow'] },
{ key: 'kudos', label: 'Kudos', icon: '★', types: ['favourite'] },
{ key: 'reposts', label: 'Reposts', icon: '↻', types: ['reblog'] },
]
const active = $derived(FOLDERS.find((entry) => entry.key === folder) ?? FOLDERS[0])
const isRequests = $derived(active.key === 'requests')
let notifications = $state<Feed<Notification>>(new Feed<Notification>(async () => ({ items: [], links: {} })))
let requests = $state<Feed<Account>>(new Feed<Account>(async () => ({ items: [], links: {} })))
let busyIds = $state<Record<string, boolean>>({})
let actionError = $state<string | null>(null)
const VERB: Record<string, string> = {
mention: 'sent you a message',
follow: 'added you as a friend',
follow_request: 'wants to be your friend',
favourite: 'gave your entry kudos',
reblog: 'reposted your entry',
poll: 'closed a poll you voted in',
status: 'posted a new entry',
update: 'edited an entry you interacted with',
'pleroma:emoji_reaction': 'reacted to your entry',
}
$effect(() => {
const key = active.key
const types = active.types
if (!session.signedIn) return
untrack(() => {
if (key === 'requests') {
requests = new Feed<Account>((cursor) => endpoints.fetchFollowRequests(session.api, cursor))
void requests.reload()
} else {
notifications = new Feed<Notification>((cursor) =>
endpoints.fetchNotifications(session.api, cursor, types.length > 0 ? types : undefined),
)
void notifications.reload()
}
document.title = `${key === 'requests' ? 'Friend Requests' : 'Mail Center'} | plspace`
})
})
async function respond(account: Account, approve: boolean): Promise<void> {
if (busyIds[account.id]) return
busyIds = { ...busyIds, [account.id]: true }
actionError = null
try {
await (approve ? endpoints.authorizeFollowRequest : endpoints.rejectFollowRequest)(
session.api,
account.id,
)
requests.remove(account.id)
} catch (cause) {
actionError = cause instanceof Error ? cause.message : 'That didnt work.'
} finally {
busyIds = { ...busyIds, [account.id]: false }
}
}
</script>
<div class="page mail-page" data-folder={active.key}>
<h1 class="page-title">Mail Center</h1>
<p class="page-subtitle">
{isRequests ? 'Friend Request Manager' : active.label}
</p>
{#if !session.signedIn}
<p class="notice">
<a href="#/login">Sign in</a> to read your mail.
</p>
{:else}
<div class="layout--split">
<div class="layout-column layout-column--left">
<Module title="Folders" flush>
<ul class="mail-folders">
{#each FOLDERS as entry (entry.key)}
<li class="mail-folder">
<a
class="mail-folder-link"
href={entry.key === 'inbox' ? '#/mail' : `#/mail/${entry.key}`}
aria-current={entry.key === active.key ? 'page' : undefined}
>
<span class="action-list-icon" aria-hidden="true">{entry.icon}</span>
<span class="mail-folder-label">{entry.label}</span>
</a>
</li>
{/each}
</ul>
</Module>
</div>
<div class="layout-column layout-column--main">
{#if actionError}
<p class="error-note" role="alert">{actionError}</p>
{/if}
{#if isRequests}
<Module title="Approve or Deny Your Friend Requests" variant="band">
{#if requests.items.length > 0}
<p class="mail-listing-count">
Listing 1&ndash;{requests.items.length} of {requests.items.length}
</p>
{/if}
<table class="mail-table">
<thead>
<tr>
<th scope="col">From</th>
<th scope="col">Confirmation</th>
</tr>
</thead>
<tbody>
{#each requests.items as account (account.id)}
<tr class="mail-row" data-kind="follow_request" data-account={account.acct}>
<td class="mail-table-from">
<a href={profilePath(account)}>
<Avatar {account} plain />
</a>
</td>
<td>
<strong>
<a href={profilePath(account)}>{displayNameOf(account)}</a>
</strong>
wants to be your friend!
<div class="person-row-handle">{fullHandle(account, session.host)}</div>
<div class="mail-table-actions">
<button
type="button"
class="button"
disabled={busyIds[account.id]}
onclick={() => void respond(account, true)}
>
Approve
</button>
<button
type="button"
class="button"
disabled={busyIds[account.id]}
onclick={() => void respond(account, false)}
>
Deny
</button>
<a class="button" href={`#/compose?to=${encodeURIComponent(account.acct)}`}>
Send Message
</a>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
<Pager feed={requests} label="View More Requests" emptyText="No pending friend requests." />
</Module>
{:else}
<Module title={active.label} variant="band">
<table class="mail-table">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">From</th>
<th scope="col">Subject</th>
</tr>
</thead>
<tbody>
{#each notifications.items as item (item.id)}
<tr class="mail-row" data-kind={item.type} data-account={item.account.acct}>
<td class="mail-table-date">{stampDate(item.created_at)}</td>
<td class="mail-table-from">
<a href={profilePath(item.account)}>
<Avatar account={item.account} plain />
</a>
</td>
<td class="mail-table-subject">
<strong>
<a href={profilePath(item.account)}>{displayNameOf(item.account)}</a>
</strong>
{VERB[item.type] ?? item.type}
{#if item.status}
<p class="mail-table-excerpt">
<a href={`#/blog/${item.status.id}`}>
{toPlainText(item.status.spoiler_text || item.status.content).slice(0, 140) ||
'(no text)'}
</a>
</p>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
<Pager feed={notifications} label="View More Mail" emptyText="Your inbox is empty." />
</Module>
{/if}
</div>
</div>
{/if}
</div>