feat: follow requests

This commit is contained in:
mae taylor 2025-07-14 18:45:38 +01:00
parent 563541d0e6
commit e3586f4eec
Signed by: mae
GPG key ID: 3C80D76BA7A3B9BD
4 changed files with 179 additions and 3 deletions

View file

@ -38,6 +38,10 @@
"back": "Back"
},
"follow_requests": {
"none": "no follow requests to action right now!"
},
"timeline": {
"home": "Home",
"local": "Local",

View file

@ -202,6 +202,40 @@ export async function getFollowRequests(host, token, since_id, max_id, limit) {
return data;
}
/**
* POST /api/v1/follow_requests/:account_id/authorize
* @param {string} host - The domain of the target server.
* @param {string} token - The application token.
* @param {string} account_id - The account ID of the follow request to accept
*/
export async function acceptFollowRequest(host, token, account_id) {
let url = `https://${host}/api/v1/follow_requests/${account_id}/authorize`;
const data = await fetch(url, {
method: 'POST',
headers: { "Authorization": "Bearer " + token }
}).then(res => res.json());
return data;
}
/**
* POST /api/v1/follow_requests/:account_id/reject
* @param {string} host - The domain of the target server.
* @param {string} token - The application token.
* @param {string} account_id - The account ID of the follow request to reject
*/
export async function rejectFollowRequest(host, token, account_id) {
let url = `https://${host}/api/v1/follow_requests/${account_id}/reject`;
const data = await fetch(url, {
method: 'POST',
headers: { "Authorization": "Bearer " + token }
}).then(res => res.json());
return data;
}
/**
* GET /api/v1/timelines/{timeline}
* @param {string} host - The domain of the target server.

View file

@ -3,6 +3,7 @@ import { writable } from "svelte/store";
import * as api from "./api.js";
import { app } from './client/app.js';
import { get } from 'svelte/store';
import { parseAccount } from './account.js';
// Cache for all requests
export let followRequests = writable();
@ -20,5 +21,8 @@ export async function fetchFollowRequests(force) {
get(app).token
);
// parse accounts
newReqs = newReqs.map((r) => parseAccount(r));
followRequests.set(newReqs);
}

View file

@ -1,10 +1,144 @@
<script>
import { followRequests } from '$lib/followRequests.js';
import PageHeader from '../../lib/ui/core/PageHeader.svelte';
import Lang from '$lib/lang';
import {server} from '$lib/client/server';
import {app} from '$lib/client/app';
import Button from '../../lib/ui/Button.svelte';
import * as api from '$lib/api'
import TickIcon from '../../img/icons/tick.svg'
import CrossIcon from '../../img/icons/cross.svg'
import { get } from 'svelte/store';
const lang = Lang('en_GB');
async function actionRequest(account_id, approved) {
// remove item from array first - this updates the ui and
// makes the interaction more seamless
$followRequests.splice(
$followRequests.indexOf(
$followRequests.find(r => r.id)
),
1
);
// hack: force the state to update now that we just spliced the array
$followRequests = $followRequests
if(approved) {
await api.acceptFollowRequest(
get(server).host,
get(app).token,
account_id
)
} else {
await api.rejectFollowRequest(
get(server).host,
get(app).token,
account_id
)
}
}
// aliases
const acceptRequest = (id) => actionRequest(id, true);
const denyRequest = (id) => actionRequest(id, false);
</script>
<div>
<PageHeader title={lang.string('navigation.follow_requests')}/>
{#if $followRequests.length < 1}
<p class="request-zero">{lang.string('follow_requests.none')}</p>
{/if}
{#each $followRequests as req}
<div class="request">
<a href="/{$server.host}/@{req.fqn}" class="request-avatar-container" on:mouseup|stopPropagation>
<img src={req.avatar_url} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async">
</a>
<div class="info">
<div class="request-user-info">
<a href="/{$server.host}/@{req.fqn}" class="name">{@html req.rich_name}</a>
<span class="username">{req.mention}</span>
</div>
</div>
<div class="request-options">
<Button filled title="Yes" on:click={() => acceptRequest(req.id)}>
<TickIcon width="24px"/>
</Button>
<Button title="No" on:click={() => denyRequest(req.id)}>
<CrossIcon width="24px"/>
</Button>
</div>
</div>
{/each}
<style></style>
<style>
.request {
width: 100%;
display: flex;
flex-direction: row;
background: var(--bg-900);
padding: .5rem;
border-radius: 8px;
}
.request a,
.request a:visited {
color: inherit;
text-decoration: none;
}
.request a:hover {
text-decoration: underline;
}
.request-avatar-container {
margin-right: 12px;
display: flex;
}
.post-avatar {
border-radius: 8px;
}
.info {
display: flex;
flex-grow: 1;
flex-direction: row;
}
.request-user-info {
margin-top: -2px;
display: flex;
flex-direction: column;
justify-content: center;
}
.request-user-info a {
display: block;
}
.request-user-info .username {
opacity: .8;
font-size: .9em;
}
.request-options {
display: flex;
gap: 8px;
}
.request-options :global(button) {
width: fit-content;
height: 100%;
}
.request-zero {
opacity: 0.8;
font-size: 0.95rem;
text-align: center;
}
</style>