mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
241 lines
8.9 KiB
Svelte
241 lines
8.9 KiB
Svelte
<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 didn’t 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–{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>
|