mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<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 { session } from '$lib/stores/session.svelte'
|
||||
import { Feed } from '$lib/stores/feed.svelte'
|
||||
import {
|
||||
authorizeFollowRequest,
|
||||
fetchFollowRequests,
|
||||
fetchNotifications,
|
||||
rejectFollowRequest,
|
||||
} from '$lib/api/endpoints'
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
folder?: string
|
||||
}
|
||||
|
||||
let { folder = 'inbox' }: Props = $props()
|
||||
|
||||
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) => fetchFollowRequests(session.api, cursor))
|
||||
void requests.reload()
|
||||
} else {
|
||||
notifications = new Feed<Notification>((cursor) =>
|
||||
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 ? authorizeFollowRequest : 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)}>
|
||||
<img src={account.avatar_static || account.avatar} alt="" loading="lazy" />
|
||||
</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)}>
|
||||
<img src={item.account.avatar_static || item.account.avatar} alt="" loading="lazy" />
|
||||
</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>
|
||||
Reference in New Issue
Block a user